1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-30 01:49:58 +02:00

520 Commits

Author SHA1 Message Date
Ryan Cramer
941ef5cb67 Update to 2018 2018-03-16 10:44:38 -04:00
Ryan Cramer
48fe0769a4 Remove console.log from ProcessProfile.js 2018-03-16 10:35:56 -04:00
Ryan Cramer
4551748659 Bump version to 3.0.96 2018-03-16 10:30:08 -04:00
Ryan Cramer
f52c3f54c9 Fix issue processwire/processwire-issues#322 where certain scenario could allow collision of language-specific page names 2018-03-16 10:24:46 -04:00
Ryan Cramer
6c4f4103d2 Fix issue processwire/processwire-issues#542 in InputfieldRepeater.module repeater item titles, where numbered HTML entities (like the one for apostrophe) were getting their "#" character removed, making it look like double encoding 2018-03-16 07:40:36 -04:00
Ryan Cramer
19a55c6d37 Enhancement for issue processwire/processwire-issues#539 show "Edit Profile" button after login when user has profile-edit permission but not page-edit permission 2018-03-15 11:02:37 -04:00
Ryan Cramer
ba21b28b4e Attempt fix for issue processwire/processwire-issues#537 update code that corrects Firefox issue where it populates autocomplete password field even when autocomplete is disabled (Firefox ignores disabled autocomplete here for some reason) 2018-03-15 09:39:03 -04:00
Ryan Cramer
bd72c59e41 Fix issue processwire/processwire-issues#535 missing null check for return value from getInputfield() in ProcessField 2018-03-15 08:55:27 -04:00
Ryan Cramer
eb95498183 Fix issue processwire/processwire-issues#534 PageList narrow mode didn't show 'New' action for home, or 'Empty' action for trash 2018-03-15 08:48:21 -04:00
Ryan Cramer
d9b30167c7 Typo fixes per processwire/processwire-issues#533 2018-03-14 11:50:50 -04:00
Ryan Cramer
bbfb4d78d2 Update for additional suggestions in processwire/processwire-issues#518 2018-03-14 10:37:24 -04:00
Ryan Cramer
27b6141a1b Fix issue processwire/processwire-issues#531 repeater within a fieldset depth changes unexpectedly when repeater item dragged up/down. 2018-03-14 10:02:15 -04:00
Ryan Cramer
1045acc057 Make PageTable buttons have better margins per processwire/processwire-issues#521 2018-03-14 08:43:56 -04:00
Ryan Cramer
11398f00bf Update for AdminThemeUikit main.js per processwire/processwire-issues#480 2018-03-14 08:13:46 -04:00
Ryan Cramer
c1ca1fde3b Fix minor JS issue where ajax-loaded inputfields didn't have the toggle arrow icon properly adjusting for open/closed state 2018-03-13 09:40:04 -04:00
Ryan Cramer
0a98458d5b Fix issue where ajax-loaded Inputfield render() didn't have before(render) hooks called when rendering the non-ajax placeholder, which was identified in processwire/processwire-issues#480 2018-03-13 09:38:32 -04:00
Ryan Cramer
3a76db94f9 Some minor optimizations in WireHooks class, plus enable before/after option for $type argument in runHooks method that was mentioned in the phpdoc but wasn't fully supported yet 2018-03-13 09:37:05 -04:00
Ryan Cramer
d82d011eca Change ksort to sort in minColumnWidth for AdminThemeUikit 2018-03-12 13:33:56 -04:00
Ryan Cramer
d99c5ec731 Fix processwire/processwire-issues#526 where part of longclick for LanguageTabs was still active (was converted to double click awhile back) 2018-03-12 10:37:29 -04:00
Ryan Cramer
ceff2e47a8 Fix issue processwire/processwire-issues#523 prevent user from uninstalling admin themes defined by $config->defaultAdminTheme 2018-03-12 10:16:11 -04:00
Ryan Cramer
afe41773d8 Fix issue processwire/processwire-issues#519 where front-edit editor w/CKEditor inline image action was not working 2018-03-12 09:45:03 -04:00
Ryan Cramer
20f02cddf9 Some AdminThemeUikit updates, mostly related to processwire/processwire-issues#480 2018-03-12 09:02:00 -04:00
Ryan Cramer
0c760171bd Bump version to 3.0.95 2018-03-09 12:26:29 -05:00
Ryan Cramer
36bceb6ed2 Fix issue in Selectize.js processwire/processwire-issues#520 where Selectize.js had bottleneck in measureString() function that caused a slowdown because of DOM manipulation (was noticed when Selectize paired with Uikit 3) 2018-03-09 09:17:04 -05:00
Ryan Cramer
5da4e17a27 Update for processwire/processwire-issues#517 and ryancramerdesign/AdminThemeUikit#46 correct issue where InputfieldImage defined buttonClass wasn't getting used in repeaters, plus update the Uikit custom button class to not be applied when InputfieldImage::renderButtons() is hooked, in order to avoid two different button styes appearing when other modules add buttons to it. 2018-03-08 10:56:32 -05:00
Ryan Cramer
bd3be5e315 Fix issue processwire/processwire-issues#518 where multi-instance + multi-site had boot error in Windows 2018-03-08 08:58:15 -05:00
Ryan Cramer
786a4b8309 Update InputfieldAsmSelect to use font-awesome based edit icon rather than a jQuery UI one 2018-03-07 13:07:38 -05:00
Ryan Cramer
3f62968392 Resolve various minor AdminThemeUikit issues and update Uikit 3 version to beta 40 2018-03-07 13:06:08 -05:00
Ryan Cramer
787c589151 Fix issue processwire/processwire-issues#516 where the WireDateTime conversion table from date to strftime incorrectly had '$' rather than '%' for some of the conversions. 2018-03-07 08:26:43 -05:00
Ryan Cramer
b1454b31a4 Add ProcessField detection for invalid Page field showIf/requiredIf dependencies per processwire/processwire-issues#509 2018-03-07 08:11:42 -05:00
Ryan Cramer
a166c22e71 Update for processwire/processwire-issues#480 2018-03-07 07:22:44 -05:00
Ryan Cramer
16f614f5a0 Fix issue processwire/processwire-issues#513 datepicker z-index when near image field 2018-03-06 11:04:15 -05:00
Ryan Cramer
0aefc2baa3 Fix issue processwire/processwire-issues#512 correct unnecessary error message when performing file field schema update 2018-03-06 10:53:54 -05:00
Ryan Cramer
aada758d14 Update AdminThemeUikit JS for Inputfield widths show/hide calculations per processwire/processwire-issues#480 2018-03-06 09:01:56 -05:00
Ryan Cramer
e20562b5c1 Add @horst-n improvements to ImageSizerEngine and InputfieldImage focus+zoom features, plus add some other adjustments and improvements in InputfieldImage.js 2018-03-05 08:45:39 -05:00
Ryan Cramer
a9ec5a640a Bump version to 3.0.94 2018-03-02 12:01:16 -05:00
Ryan Cramer
76f3dac6fd Update for processwire/processwire-issues#408 plus some phpdoc updates in ProcessModule to appease phpstorm 2018-03-01 09:30:45 -05:00
Ryan Cramer
cd0f76d32c Fix issue processwire/processwire-issues#505 add support for detection of missing closing tags in WireMarkupRegions in order to prevent timeout 2018-02-28 08:40:58 -05:00
Ryan Cramer
8acd06646f Fix issue processwire/processwire-issues#349 InputfieldOptions with InputfieldSelect required field not having blank option 2018-02-27 10:36:05 -05:00
Ryan Cramer
5674af8b99 Minor count() update per processwire/processwire-issues#408 plus some minor phpdoc updates to appease phpstorm 2018-02-27 08:33:17 -05:00
Ryan Cramer
7c0012fbd9 Additional wireCount updates per processwire/processwire-issues#408 2018-02-26 10:16:12 -05:00
Ryan Cramer
ac779c79a8 Fix issue processwire/processwire-issues#302 where markup <region> tags didn't get removed if template file produced no output 2018-02-26 09:58:29 -05:00
Ryan Cramer
9d2a986048 Add @Toutouwai request related to processwire/processwire-issues#84 for improving touch input detection in ProcessLogin via method like InputDetect module with whatinput JS. 2018-02-26 09:19:28 -05:00
Ryan Cramer
37f95199f4 Add descriptive error message when trying to use OR values with path/URL and core PathPaths module is not installed, per processwire/processwire-issues#504 2018-02-26 06:27:29 -05:00
Ryan Cramer
869040bb30 Attempt to fix issue processwire/processwire-issues#503 incorrect label index warning/notice in ProcessRecentPages 2018-02-26 06:00:52 -05:00
Ryan Cramer
bd90821d40 Minor adjustment to account for possibility that CRLF may get counted as two bytes in InputfieldTextarea::getTextLength method (for maxlength detection). 2018-02-25 07:40:03 -05:00
Ryan Cramer
f3c188254d Bump version to 3.0.93 2018-02-23 11:51:54 -05:00
Ryan Cramer
72e00189b5 Some adjustments to InputfieldCKEditor for future support of external asset pages. Not currently enabled, just getting some foundational stuff in place for future use. 2018-02-23 10:56:32 -05:00
Ryan Cramer
dd9f215247 Update LanguageSupport module config screen to recommend next steps during module installation 2018-02-23 10:36:38 -05:00
Ryan Cramer
919c475631 Various minor adjustments 2018-02-23 10:36:15 -05:00
Ryan Cramer
053ef62970 Fix issue processwire/processwire-issues#500 where trackChanges state of cloned page was off 2018-02-21 06:12:39 -05:00
Ryan Cramer
b2281d6e93 Optimize WireMail module detection, add support for replyTo() method, add $config->wireMail default settings for WireMail module(s). processwire/processwire-issues#498 2018-02-20 10:12:57 -05:00
Ryan Cramer
fc1c6e5ea1 Fix issue processwire/processwire-issues#499 as well as expand upon phpdoc in ProcessPageEditImageSelect.module 2018-02-20 06:37:35 -05:00
Ryan Cramer
8fe1eb13f4 Add support for interactive selection of zoom, combined with focus, in InputfieldImage. To enable zoom, go to your image field settings, and on the Input tab locate the "Focus point selection" field and choose "Focus point and zoom". Bump version to 3.0.92. 2018-02-16 11:28:25 -05:00
Ryan Cramer
5c6e54e24d Fix issue processwire/processwire-issues#495 update word counter to display max length even when field is blank 2018-02-14 09:49:40 -05:00
Ryan Cramer
5493d5fea6 Add @horst-n support for focus zoom setting in the ImageSizerEngines 2018-02-13 06:02:30 -05:00
Ryan Cramer
e73ec872da Fix issue processwire/processwire-issues#491 add support for automatically cloning fieldset _END field when fieldset is cloned 2018-02-12 09:20:38 -05:00
Ryan Cramer
ef6a9d56e4 Fix issue processwire/processwire-issues#492 where FieldtypeTextareaLanguage fields on multi-language install with only default language present could get caught in a loop during load of field 2018-02-12 09:12:43 -05:00
Ryan Cramer
1f6ae656dc Bump version to 3.0.91 2018-02-09 10:10:09 -05:00
Ryan Cramer
0630b3a3c3 Add support for PageFinder selectors where field name is the name of a Fieldtype module. During search, it translates to all fields having that Fieldtype. This enables you to perform a search on all text/textarea fields at once, for example, "FieldtypeTextarea%=something". You can also do "FieldtypeText.extends%=something", and that will include all fields that use or extend the given Fieldtype. Or you can do "FieldtypeText.fields%=something" and the resulting PageArray will contain a "fields" data property that contains an array of matched fields and the quantity of pages that matched each. You can also do "FieldtypeText.extends.fields%=something" and it will include both behaviors mentioned here. Finally, you can use OR expressions here too, like "FieldtypeText|FieldtypeTextarea%=something". Note that these searches are not particularly fast and may have limits on scale. The "FieldtypeName.fields" option is slower than without the "fields" option, BUT it enables it to scale further. In my testing so far, the %= operator actually performs quite a bit better here for text-based searches than the *= or ~= operators, likely because the resulting query probably really limits use of the fulltext indexes. 2018-02-09 09:58:49 -05:00
Ryan Cramer
bafe3d4a12 Fix issue processwire/processwire-issues#490 where ProcessPageEditLink didn't allow for external link rel attributes with space separated values 2018-02-08 09:22:05 -05:00
Ryan Cramer
6dbb244654 Fix issue processwire/processwire-issues#488 where UTF-8 mode urlSegments that contained only ascii would get converted to lowercase 2018-02-08 08:51:33 -05:00
Ryan Cramer
3ade2c3269 Fix issue processwire/processwire-issues#489 where file/image field with limit=1, multi-language description was not retained on when file was replaced 2018-02-08 08:07:50 -05:00
Ryan Cramer
cf292363ca Enhance wireBytesStr() function with more options per processwire/processwire-requests#133 2018-02-06 10:03:13 -05:00
Ryan Cramer
3918bba0ab Upgrade htmlpurifier version to 4.9.3 per processwire/processwire-issues#483 2018-02-06 05:44:43 -05:00
Ryan Cramer
eb80f52efe Fix issue processwire/processwire-issues#487 where svg files in $config->pageFileSecure mode needed entry in $config->fileContentTypes 2018-02-06 05:39:19 -05:00
Ryan Cramer
5c708797f9 Update for processwire/processwire-issues#408 PHP 7.2 and changed behavior of count() 2018-02-05 06:25:05 -05:00
Ryan Cramer
f89544ea04 Fix issue processwire/processwire-issues#485 where attempting to use numeric index with WireData could halt request/memory error 2018-02-05 06:09:54 -05:00
Ryan Cramer
e21c965f7b Fix issue processwire/processwire-issues#484 where PageFrontEdit with <edit field="pageID.fieldName"> tag was not working property. 2018-02-05 05:57:15 -05:00
Ryan Cramer
8e52ee8097 Bump version to 3.0.90 2018-02-02 15:00:29 -05:00
Ryan Cramer
461152a8cd This update to AdminThemeUikit fixes various minor reported issues with Inputfields and some other small details 2018-02-02 10:36:11 -05:00
Ryan Cramer
cb2683208f Various minor adjustments 2018-02-02 10:16:15 -05:00
Ryan Cramer
3eba1b58aa Fix issues processwire/processwire-issues#173 and processwire/processwire-issues#393 nested repeater labels 2018-02-02 10:10:49 -05:00
Ryan Cramer
1d82b530ff Fix issue processwire/processwire-issues#347 where field export that included roles was using IDs rather than role names. Also same issue with PageTable that was using template IDs rather than template names. 2018-02-02 10:05:40 -05:00
Ryan Cramer
f7ff7eab8c Fix issue processwire/processwire-issues#352 where deleted role could interfere with the "who can access this page" field in the page editor 2018-02-01 11:39:30 -05:00
Ryan Cramer
bbca8f5669 Add new module ImageSizerEngineAnimatedGif by @horst-n for animated GIF support in image resizes 2018-01-31 09:13:21 -05:00
Ryan Cramer
03660974ee Some additional helpful logic and messages added in ProcessRole, plus an update to resolve processwire/processwire-issues#474 2018-01-31 08:10:32 -05:00
Ryan Cramer
e917c93d76 Fix issue processwire/processwire-issues where clone of page having FieldsetPage field was not cloning the FieldsetPage portion of it 2018-01-29 09:16:03 -05:00
Ryan Cramer
90efe9b14a Bump version to 3.0.89 and some other adjustments 2018-01-26 11:43:22 -05:00
Ryan Cramer
2cebe89e57 Fix issue processwire/processwire-issues#475 adjustment to argument type hint in ProcessUser 2018-01-25 09:50:11 -05:00
Ryan Cramer
02f1eadacc Fix issue processwire/processwire-issues#474 where ProcessRole wasn't working correctly with page-view permission assignment 2018-01-25 09:43:16 -05:00
Ryan Cramer
2a946c09de Fix issue processwire/processwire-issues#473 where SelectableOptionManager.php:addOptions() method had incorrect "array" type hint in method argument 2018-01-25 06:26:52 -05:00
Ryan Cramer
b72e609cc4 Fix issue processwire/processwire-issues#468 where ProcessPageSearch wasn't working with ^= (starts with) search operator 2018-01-25 06:22:51 -05:00
Ryan Cramer
a021d3b54f Fix issue processwire/processwire-issues#467 correct typo in PaginatedArray sprintf string 2018-01-25 06:15:23 -05:00
Ryan Cramer
7b1913b6ed Add support for image focus area / focus point / focal point (or whatever the best term is) to InputfieldImage per processwire/processwire-requests#150 2018-01-24 11:48:50 -05:00
Ryan Cramer
3fb3294f5a Add support for filedata in FieldtypeFile/FieldtypeImage, which provides a way to store other general purpose data with files/images. 2018-01-24 11:31:03 -05:00
Ryan Cramer
6241fd6730 Fix issue with InputfieldSelector not recognizing template_ids property on Page reference fields that use autocomplete 2018-01-14 11:55:54 -05:00
Ryan Cramer
b3ea132d81 Fix ProcessTemplate and asmSelect issue processwire/processwire-issues#472 2018-01-11 09:51:47 -05:00
Ryan Cramer
ad6f6bd283 Fix JqueryUI modal.js issue processwire/processwire-issues#471 2018-01-11 09:39:06 -05:00
Ryan Cramer
f7b49055cd Fix PageFrontEdit issue processwire/processwire-issues#470 2018-01-11 09:36:46 -05:00
Ryan Cramer
f3749d241a Change an sanitizer entities() call to entities1() since it's getting double encoded for some reason in @adrianbj case 2018-01-05 12:37:40 -05:00
Ryan Cramer
4f7981cf75 Bump version to 3.0.88 2018-01-05 12:05:35 -05:00
Ryan Cramer
de5c241b6a Fix issue and PR #54 with similar solution though also remove predefined min/max image settings from ProcessPageEditImageSelect because they seem to be causing various issues, and the predefined settings are more about uploaded files. 2018-01-05 10:56:05 -05:00
Ryan Cramer
fb39ded94d Add support for image file actions to InputfieldImage and update ImageSizer engines to support new rotate, flip and color actions 2018-01-05 10:54:21 -05:00
Ryan Cramer
9bdad6fc86 Detect NullPage in InputfieldCKEditor::getImagesFieldName() to fix issue that appeared for someone when editing page with no images fields. 2017-12-31 11:02:42 -05:00
Ryan Cramer
a31d4a84e2 Upgrade CKEditor to 4.8.0, add support for direct image upload/paste in CKEditor. Bump version to 3.0.87. 2017-12-29 08:42:49 -05:00
Ryan Cramer
5bba89b043 Fix issue processwire/processwire-issues#462 solution for files using arabic filenames 2017-12-26 07:27:50 -05:00
Ryan Cramer
f573473066 Add support for custom configuration of what Fieldtype/Inputfield settings may be overridden for field/template context. Appears only in $config->advanced mode. You can see it when editing a field (ProcessField) on the "Overrides" tab. Related to processwire/processwire-requests#145 2017-12-22 11:11:02 -05:00
Ryan Cramer
63229872a0 Fix typo in phpdoc processwire/processwire-issues#458 2017-12-22 07:47:44 -05:00
Ryan Cramer
5b53a41c12 Fix issue processwire/processwire-issues#456 2017-12-21 09:36:17 -05:00
Ryan Cramer
87dc586c8c Add PageFinder selector support for field.owner.subfield=value where "field" is a PageTable, Page or Repeater field, and "owner" is a literal reserved word, and "subfield" is any field or field.subfield combination. It makes the selector match Repeater, PageTable or Page reference pages that have an "owner" that matches. This "owner" would be defined as the page that has a Repeater or PageTable item being matched, as one of its fields. Ping @apeisa 2017-12-20 12:21:14 -05:00
Ryan Cramer
362c0a0af5 Bump version to 3.0.86 2017-12-15 15:28:40 -05:00
Ryan Cramer
aa1f7dbd47 Fix issue processwire/processwire-issues#455 2017-12-15 09:19:18 -05:00
Ryan Cramer
ded6476832 Fix issue processwire/processwire-issues#454 for Pageimage::maxSize() 2017-12-15 06:30:36 -05:00
Ryan Cramer
77ad382bcd Attempt fix for issue processwire/processwire-issues#452 2017-12-15 06:05:52 -05:00
Ryan Cramer
04b7dd0404 Fix autoload_noget change tracking issue in Page per @apeisa 2017-12-14 06:38:08 -05:00
Ryan Cramer
9b10232b73 Add ability to specify roles that aren't allowed to login, related to processwire/processwire-requests#140 plus while I was in there, did some re-working of login related code in Session class and user management code in ProcessUser.module. 2017-12-13 10:37:39 -05:00
Ryan Cramer
bac60dc340 Add support for interlaced jpeg images per @horst-n and processwire/processwire-requests#134 2017-12-11 11:09:57 -05:00
Ryan Cramer
67cdf1d456 Add variation of @adrianbj PR#92 which adds Imagick as another method or determining SVG dimensions 2017-12-11 10:49:59 -05:00
Ryan Cramer
b4b40f19f4 Fix issue processwire/processwire-issues#449 2017-12-11 05:50:48 -05:00
Ryan Cramer
763c713f4c wireCount updates per processwire/processwire-issues#408 2017-12-11 05:42:48 -05:00
Ryan Cramer
927f66cc61 Bump version to 3.0.85 2017-12-08 11:30:53 -05:00
Ryan Cramer
7a3da093d9 Update InputfieldCheckbox to support a separately configured checkbox label, distinct from the field label (for the cases where you might want both). 2017-12-08 10:20:20 -05:00
Ryan Cramer
93a5cc20b1 Update random string generation method in Password class (randomBase64String) to support new PHP7 random_bytes function, plus add a test mode for testing all generation techniques at once. processwire/processwire-issues#447 2017-12-08 09:36:25 -05:00
Ryan Cramer
dfb5f740d0 Add usage example to FieldtypeOptions config screen 2017-12-08 09:32:46 -05:00
Ryan Cramer
1ba1323e81 count() to wireCount() update per processwire/processwire-issues#408 2017-12-08 07:55:34 -05:00
Ryan Cramer
76e956ad61 Additional updates for autojoin related issue, delay autojoin field sanitization until first access of field 2017-12-06 06:33:28 -05:00
Ryan Cramer
00a8bf03d3 Fix isSaveable() unnecessary parent load + findMany() issue per @apeisa 2017-12-06 05:58:08 -05:00
Ryan Cramer
45eadd971b Fix Page field autojoin issue per @apeisa 2017-12-06 05:45:07 -05:00
Ryan Cramer
fb21af434d Fix issue processwire/processwire-issues#443 2017-12-05 12:15:23 -05:00
Ryan Cramer
5b8761454a Fix issue processwire/processwire-issues#439 2017-12-05 10:51:39 -05:00
Ryan Cramer
3b21a3e9a8 Add @teppokoivula suggested fix for processwire/processwire-issues#435 PageFrontEnd when editing 404 page 2017-12-04 11:17:16 -05:00
Ryan Cramer
5bbd342570 Fix issue processwire/processwire-issues#433 2017-12-04 10:57:04 -05:00
Ryan Cramer
06c1a46ba5 Fix issue processwire/processwire-issues#428 2017-12-04 10:28:51 -05:00
Ryan Cramer
a865e0a053 Some cleanup in ProcessPageAdd plus add support for disabling template suggestions when adding pages per processwire/processwire-issues#424 via $config->pageAdd('noSuggestTemplates', true); or specify space-separated list of template names (string) for the "true" value. 2017-12-04 09:51:11 -05:00
Ryan Cramer
337e59663b Add support for group notifications in AdminThemeFramework (AdminThemeUikit), along with a config option in the AdminThemeUikit module. Also fix an issue in ProcessProfile that was interfering with ajax requests. 2017-11-20 09:45:16 -05:00
Ryan Cramer
32b3da7b0d Fix issue where collapsed repeater in fieldset (where repeater is only field in fieldset), when clicking to un-collapse repeater, it would jump to top of page 2017-11-19 06:33:22 -05:00
Ryan Cramer
81a07fde34 Attempt to fix issue processwire/processwire-issues#432 for disabling autocomplete and password field 2017-11-18 07:00:48 -05:00
Ryan Cramer
22c69ec599 Add latest version of AdminThemeUikit and bump version to 3.0.84 2017-11-17 10:02:49 -05:00
Ryan Cramer
2c4c9de61d Fixes to ryancramerdesign/AdminThemeUikit#55 and ryancramerdesign/AdminThemeUikit#61 (when combined with latest AdminThemeUikit version, to be added shortly) 2017-11-17 09:44:45 -05:00
Ryan Cramer
60572a4cd2 Fix issue with PageTable not having correct table styles when using AdminThemeUikit 2017-11-17 08:29:22 -05:00
Ryan Cramer
7d16590f07 Update ProcessProfile to support the ability to edit user name (if configured to do so). Also update it so that it requires you to enter your password before you can commit changes to email or user name. 2017-11-16 10:54:41 -05:00
Ryan Cramer
6c3eb6a460 Update ProcessTemplate so that it lets you specify when fields user can edit in their profile, when editing the "user" template. Meaning, it's no longer necessary to jump back and forth between ProcessProfile and the ProcessProfile module config. 2017-11-16 10:53:16 -05:00
Ryan Cramer
ac59da3427 Upate JqueryWireTabs module to support more customization of its tooltips (so admin theme can adjust) 2017-11-16 10:52:13 -05:00
Ryan Cramer
ebc150a3a7 Update PW installer to use Uikit 3 theme, plus add support for detecting and dropping existing tables (if present) during installation. 2017-11-16 10:50:36 -05:00
Ryan Cramer
58eb0c3159 Various minor adjustments 2017-11-16 10:49:26 -05:00
Ryan Cramer
221a15a653 Add AdminThemeUikit module to core and bump version to 3.0.83 2017-11-10 11:10:40 -05:00
Ryan Cramer
e0d04a4626 Upgrade InputfieldPassword to support requirement of entering your old password before it'll let you set a new one. 2017-11-10 09:13:10 -05:00
Ryan Cramer
840ab68625 Minor cosmetic adjustments 2017-11-10 09:11:00 -05:00
Ryan Cramer
e2a42381ac Some optimizations to LanguageTabs and add support for Uikit3 beta34+ 2017-11-10 09:08:30 -05:00
Ryan Cramer
7398267be1 Bump version to 3.0.82 2017-11-03 10:51:44 -04:00
Ryan Cramer
8e27f859c0 Fix issue ryancramerdesign/AdminThemeUikit#47 where AdminThemeFramework was not running the headline through the language translation function 2017-11-03 08:10:47 -04:00
Ryan Cramer
297a64f9d4 Fix issue processwire/processwire-issues#421 fixing useless redirects from manually deleted session cookies 2017-11-02 09:50:26 -04:00
Ryan Cramer
ece560daa1 Fix issue processwire/processwire-issues#418 to fix file/image drag-to-upload in IE11 using fix provided by @Toutouwai 2017-11-02 09:37:23 -04:00
Ryan Cramer
34d15dadae Fix issue processwire/processwire-issues#420 to disable autocomplete via custom attribute for InputfieldPageName using suggestion by @Toutouwai 2017-11-02 09:29:40 -04:00
Ryan Cramer
7b19df0175 Add Setup>Fields>Export/Import support for Repeater fields. processwire/processwire-issues#416 2017-11-02 06:33:46 -04:00
Ryan Cramer
ab6d158cf9 Fix issue processwire/processwire-issues#419 which corrects InputfieldImage.js sizing of list mode using fix provided by @Toutouwai 2017-11-02 06:22:50 -04:00
Ryan Cramer
7bf27cc5e5 Fix issue processwire/processwire-issues#415 where ProcessPageEditImageSelect was not showing variations for image when it was specified as one of the skipFields in module configuration. 2017-10-31 06:25:24 -04:00
Ryan Cramer
5c09f03e8b Fix issue processwire/processwire-issues#414 language tabs and language used for placeholder attribute, though not positive this one should stay...previous behavior may be preferable depending on the case. 2017-10-30 06:22:29 -04:00
Ryan Cramer
c778056991 Fix issue processwire/processwire-issues#410 by making ProcessLanguageTranslator give preference to GET var (rather than session) with language ID, by adding part of PR #93 2017-10-30 05:48:31 -04:00
Ryan Cramer
0f4cf01ec2 Update Config.php note for imageSizerOptions per processwire/processwire-issues#409 2017-10-30 05:43:20 -04:00
Ryan Cramer
066a54fb6f Fix issue processwire/processwire-issues#411 where multiple PageListSelect in same page editor weren't working properly (thanks to @Toutouwai for the fix) 2017-10-30 05:36:26 -04:00
Ryan Cramer
c90fc3f872 Add improved Roles editor that now lets you manage permissions by template 2017-10-27 11:21:13 -04:00
Ryan Cramer
e3fc776c53 Bump version to 3.0.80 2017-10-20 09:58:29 -04:00
Ryan Cramer
b2381002be Improve error reporting ability of $modules->get(), $modules->getModule(), primarily for debugging purposes. 2017-10-20 09:45:53 -04:00
Ryan Cramer
97935d156a Add wireCount() function to duplicate behavior of count() function in PHP versions prior to 7.2 2017-10-20 09:44:21 -04:00
Ryan Cramer
56e35b55ea Add support for using repeaters in user profile editor per issue processwire/processwire-issues#407 2017-10-20 09:43:08 -04:00
Ryan Cramer
b4f977c36a Fix issue processwire/processwire-issues#396 plus update ProcessCommentsManager to be compatible with AdminThemeUikit 2017-10-19 11:15:08 -04:00
Ryan Cramer
a48f4038c8 Enhancement processwire/processwire-issues#405 add support for hooks to be used in classes that extend Wire-derived classes that aren't in the ProcessWire namespace. 2017-10-17 10:00:55 -04:00
Ryan Cramer
b0f37a306e Fix issue processwire/processwire-issues#406 correction of error message in ProcessPageSort 2017-10-17 05:44:30 -04:00
Ryan Cramer
fff501cac3 Correct an issue in Repeater that interfered with some file-upload situations in draft page versions 2017-10-17 05:33:32 -04:00
Ryan Cramer
2771c353ee Fix issue processwire/processwire-issues#403 which caused problems with file uploads in some kinds of repeaters. Also bump version to 3.0.79 2017-10-13 12:59:33 -04:00
Ryan Cramer
00c183e70f Some code adjustments in ProcessPageList to support a separate renderReady() method, as well as some tweaks per @EntitySelf evernote document 2017-10-13 08:17:32 -04:00
Ryan Cramer
5dc690af9e Fix issue processwire/processwire-issues#381 where Selectors class could get confused by a quoted value with comma when contained in an OR value 2017-10-12 11:57:26 -04:00
Ryan Cramer
58bde80a81 Fix issue processwire/processwire-issues#384 update 2016 copyright date to 2017 2017-10-12 11:15:37 -04:00
Ryan Cramer
47b87d283b Per PR #91 fix issue processwire/processwire-issues#385 where ProcessPageEditImageSelect nosize option wasn't working with InputfieldCKEditor pwimage plugin 2017-10-12 11:12:42 -04:00
Ryan Cramer
f4971ae5d4 Fix issue processwire/processwire-issues#388 where repeater templates could show as selectable options in page editor template field, when they didn't need to 2017-10-12 10:52:47 -04:00
Ryan Cramer
e0a2abea07 Fix issue processwire/processwire-issues#391 remove stray console.log() in InputfieldRepeater.js 2017-10-12 10:41:54 -04:00
Ryan Cramer
7d3dc80798 Fix issue processwire/processwire-issues#395 where renaming a repeater field didn't rename the related template and fieldgroup 2017-10-12 10:36:53 -04:00
Ryan Cramer
0a790cfb43 Fix issue processwire/processwire-issues#399 where InputfieldInteger wasn't supporting defaultValue 2017-10-12 09:44:25 -04:00
Ryan Cramer
90da2e9936 Remove old CKEditor version 2017-10-12 09:43:57 -04:00
Ryan Cramer
df17b337fd Version 3.0.78 has updates primarily specific to enabling more customization from admin themes. In this case, AdminThemeUikit, but applies to any admin theme. 2017-10-06 09:33:39 -04:00
Ryan Cramer
5a39c9efc9 Just some small updates in 3.0.77, mostly related to AdminThemeUikit updates 2017-09-29 12:18:34 -04:00
Ryan Cramer
73d7e997fe Bump version to 3.0.76 2017-09-22 12:30:28 -04:00
Ryan Cramer
f822292eb3 Some updates to ProcessPageView and additions for processwire/processwire-issues#366 to better handle URLs with unrecognized characters 2017-09-21 11:37:46 -04:00
Ryan Cramer
8bcb31359f Fix issue processwire/processwire-issues#363 where repeater with min count and unpublished min item wouldn't get saved if new non-min item was added before save 2017-09-21 06:53:13 -04:00
Ryan Cramer
d765c0d589 Fix issue processwire/processwire-issues#367 where nested FieldsetPage with Repeater/Matrix combinations could cause same non-nested FieldsetPage to not get saved 2017-09-21 06:32:02 -04:00
Ryan Cramer
7943aa1064 Update for processwire/processwire-issues#371 clarify that superuser permissions are not editable (since superuser implies user with all permissions) 2017-09-20 07:50:53 -04:00
Ryan Cramer
78e07bc6cf Enhance Page::localName() method per processwire/processwire-issues#373 2017-09-20 07:15:37 -04:00
Ryan Cramer
327576ff7a Fix issue processwire/processwire-issues#374 where save+next in page editor should not proceed to next page when there were errors on editing page 2017-09-20 07:03:02 -04:00
Ryan Cramer
fffebd9214 Update CKEditor version to 4.7.3 2017-09-20 06:52:56 -04:00
Ryan Cramer
4560ed2997 Fix issue processwire/processwire-issues#377 where install of non-native site profile with 3rd party Fieldtype modules could produce error messages at install time 2017-09-20 06:43:38 -04:00
Ryan Cramer
2a3be7209c Bump version to 3.0.75 2017-09-15 10:49:48 -04:00
Ryan Cramer
064da79bae Update InputfieldFile non-ajax upload mode to warn about file uploads that exceed the max upload size. 2017-09-15 10:48:01 -04:00
Ryan Cramer
b1048297e9 Add nocache URL to image variations modal per processwire/processwire-issues#369 2017-09-11 14:17:22 -04:00
Ryan Cramer
5a9c8fea58 Attempt fix issue processwire/processwire-issues#359 where certain repeater/repeater-matrix combinations weren't initializing propertly 2017-09-11 10:44:32 -04:00
Ryan Cramer
009a2d9342 Fix issue processwire/processwire-issues#353 remove limit on max selector quantity 2017-09-11 09:05:52 -04:00
Ryan Cramer
0bdfdeeb78 Fix issue processwire/processwire-issues#354 where a.b.c='' selector was not working right 2017-09-11 08:30:08 -04:00
Ryan Cramer
8bc3e6ad2e Improve an error message on ProcessPageEditImageSelect.module 2017-09-08 10:10:27 -04:00
Ryan Cramer
41089806e1 Some minor updates for ProcessPageLister 2017-09-08 10:09:45 -04:00
Ryan Cramer
6fef531b1a Some optimizations to FieldtypeRepeater 2017-09-08 10:09:14 -04:00
Ryan Cramer
cc34ec8752 Add support to Pages/PagesLoader for populating directly to existing PageArray 2017-09-08 10:07:25 -04:00
Ryan Cramer
d427b7f563 Fix issue in ProcessPageEditLink when a FieldtypeFieldsetPage field is on the page 2017-09-03 05:58:21 -04:00
Ryan Cramer
34519c4ad3 Minor adjustment to InputfieldRepeater 2017-09-01 11:24:19 -04:00
Ryan Cramer
70289c05a8 Typo fix 2017-09-01 11:02:42 -04:00
Ryan Cramer
b544d0a50f Bump version to 3.0.74 2017-09-01 10:59:47 -04:00
Ryan Cramer
c0e8485a77 Add FieldtypeFielsetPage module to core as part of the repeaters package 2017-09-01 10:58:52 -04:00
Ryan Cramer
3cb9c46e7d Some additional updates for repeater single mode 2017-09-01 10:58:21 -04:00
Ryan Cramer
27ee1fae15 Some adjustments to FieldtypeRepeater for single page mode and update PagesExportImport for support 2017-08-31 11:19:35 -04:00
Ryan Cramer
d9fb9cd026 Updates to FieldtypeRepeater and InputfieldRepeater to support single mode, as used by FieldtypeFieldsetPage 2017-08-30 09:37:19 -04:00
Ryan Cramer
718baae573 Various small fixes and tweaks, and improvements to code documentation in several spots 2017-08-30 09:24:57 -04:00
Ryan Cramer
aa321c6b7b Bump version to 3.0.73 2017-08-25 12:23:41 -04:00
Ryan Cramer
4a26d7626f Improve extendability of FieldtypeFieldsetOpen for FieldtypeFieldsetGroup 2017-08-25 10:33:43 -04:00
Ryan Cramer
055aa6297d Remove fieldset management from ProcessTemplate since it is now native to asmSelect 2017-08-25 10:31:10 -04:00
Ryan Cramer
eaf346df12 Upgrade ProcessField with some additional hooks that can be monitored, along with a few other tweaks 2017-08-25 10:29:04 -04:00
Ryan Cramer
e68e3be9ff Upgrade asmSelect to support fieldsets natively, rather than having it bolted on separately by ProcessTemplate 2017-08-25 10:27:50 -04:00
Ryan Cramer
e96d740586 Some additional updates and fixes for pages export/import 2017-08-25 10:18:09 -04:00
Ryan Cramer
fc2b7944a2 Some minor repeater adjustments to support derived fieldtype in progress 2017-08-25 10:15:10 -04:00
Ryan Cramer
c9fca29283 Fix issue processwire/processwire-issues#344 using page name with not-equals operator in FieldtypePage selector 2017-08-22 11:22:27 -04:00
Ryan Cramer
d841a44955 Fix issue processwire/processwire-issues#293 fix missing repeater labels in nested repeater 2017-08-22 08:51:59 -04:00
Ryan Cramer
950706935e Fix issue processwire/processwire-issues#351 Fix Process.php setViewFile() method typo, and behavior improvements 2017-08-22 06:15:27 -04:00
Ryan Cramer
e38c603ce4 Continued updates to PagesExportImport, add ZIP export/import option to ProcessPagesExportImport (with page files support), and bump version to 3.0.72 2017-08-18 11:33:06 -04:00
Ryan Cramer
74a2b001cc Add support for export/import of comments fields (FieldtypeComments) 2017-08-17 09:38:06 -04:00
Ryan Cramer
b30c6a4ec8 Fix issue processwire/processwire-issues#335 error was thrown by InputfieldImage for too large file, even if client-side resize enabled 2017-08-16 10:36:17 -04:00
Ryan Cramer
56345aff26 Fix issue processwire/processwire-issues#336 where markup regions run on SVG output caused problem 2017-08-16 10:18:40 -04:00
Ryan Cramer
c496c3b859 Fix issue processwire/processwire-issues#340 - image/file tags disappear on existing files after uploading a new file 2017-08-16 09:54:12 -04:00
Ryan Cramer
74dcd51837 Additional updates per issue processwire/processwire-issues#330 - make image replacement keep the same filename (so long as files have same extension). Also upgraded it to retain description and tags during replacement. 2017-08-16 09:02:54 -04:00
Ryan Cramer
b5b2636e01 Add Repeater field support to new Page Export/Import features. 2017-08-15 12:13:25 -04:00
Ryan Cramer
e89235b757 Add $notices->move($from, $to) method for moving notices from one Wire instance to another. 2017-08-15 12:12:03 -04:00
Ryan Cramer
7873422f9e Add ProcessPagesExportImport module to core, and bump version to 3.0.71. To install this new module, go to Modules > Refresh, then Modules > Core > Process > Pages Export/Import > Install 2017-08-11 11:06:31 -04:00
Ryan Cramer
af6ab99d87 Continued major updates to the main PagesExportImport class 2017-08-11 10:45:46 -04:00
Ryan Cramer
d000117e4b Fix issue processwire/processwire-issues#338 where WireTempDir had incorrect order of arguments to explode function 2017-08-11 10:45:06 -04:00
Ryan Cramer
a216b561d1 Upgrade the multi-language text Fieldtypes to support pages export/import 2017-08-11 10:43:49 -04:00
Ryan Cramer
edd71401ed Upgrade LanguagesPageFieldValue to support population by language-name indexed array 2017-08-11 10:42:44 -04:00
Ryan Cramer
a038debd82 Upgrade FieldtypeTextarea to support conversion of file/asset URLs when exporting pages and importing elsewhere 2017-08-11 10:40:49 -04:00
Ryan Cramer
143e42722d Upgrade FieldtypeText to support importing from multi-language to non-multi-language 2017-08-11 10:39:54 -04:00
Ryan Cramer
9e76a2b770 Update FieldtypePage to implement the Fieldtype::__importValue() method for page import support 2017-08-11 10:37:57 -04:00
Ryan Cramer
7ff278a9ff Upgrade Pagefile, FieldtypeFile and FieldtypeImage for improved Page export (exportValue) support 2017-08-11 10:36:31 -04:00
Ryan Cramer
c1f7e96185 Bump version to 3.0.70 2017-08-04 12:33:52 -04:00
Ryan Cramer
3e7b2d0273 Further updates to PagesExportImport class. 2017-08-04 10:58:25 -04:00
Ryan Cramer
dc88104277 Merge branch 'horst-n-patch-1' into dev 2017-08-02 11:29:04 -04:00
Ryan Cramer
84c033a758 Merge branch 'patch-1' of https://github.com/horst-n/processwire into horst-n-patch-1 2017-08-02 11:28:28 -04:00
Ryan Cramer
cc7005192a Update InputfieldCKEditor settings screen Toolbar field to show standard toolbar names included with CKEditor 2017-08-02 11:18:33 -04:00
Ryan Cramer
502774a65f Significant refactoring of WireTempDir class, and update PagefilesManager's use of WireTempDir 2017-08-02 11:17:34 -04:00
Ryan Cramer
a07855c9f6 Minor adjustments, mostly phpdoc related 2017-08-02 11:16:32 -04:00
Ryan Cramer
fa1ff60b97 Add 2nd pass support to Markup Regions, so that a region added in 1st pass can be manipulated by commands in the 2nd pass. Also, some optimizations to region hints, and debug mode improvements. 2017-08-02 10:56:26 -04:00
Ryan Cramer
a1e1f25dff Bump version to 3.0.69 2017-07-28 15:05:27 -04:00
Ryan Cramer
629220ffc8 Add support for closing tag hints in WireMarkupRegions. These increase speed/efficiency for large documents. 2017-07-28 13:49:49 -04:00
Ryan Cramer
4a3f09d34c Fix issue processwire/processwire-issues#330 where drag-n-drop image replacement wasn't working properly 2017-07-28 11:15:00 -04:00
Ryan Cramer
abce877f02 Fix issue processwire/processwire-issues#329 Pagefile::install() method when used with URL argument that contains a query string was screwing up the file extension in the basename 2017-07-28 10:42:20 -04:00
Ryan Cramer
8169594e79 Fix issue processwire/processwire-issues#328 typo fix 2017-07-28 10:29:32 -04:00
Ryan Cramer
277674db23 Fix issue processwire/processwire-issues#325 panel.js and URL fragments fix 2017-07-28 10:10:16 -04:00
Ryan Cramer
71896f463e Fix issue processwire/processwire-issues#320 - MarkupCache, path() method, and WireArray::debugInfo() 2017-07-28 07:10:00 -04:00
Ryan Cramer
52b8389215 Fix issue processwire/processwire-issues#319 where Page::setAndSave() was only working with custom fields and not with DB native properties 2017-07-28 06:52:23 -04:00
Ryan Cramer
0c42013c45 Fix issue processwire/processwire-issues#314 - Lister bookmarks when used with parent.title and %= operator was not working correctly 2017-07-28 06:35:53 -04:00
Ryan Cramer
f5d8633590 Adjustment in Wire class and bump version to 3.0.68 2017-07-21 13:08:05 -04:00
Ryan Cramer
506f66b64a Updates to PagesExportImport class 2017-07-21 13:06:29 -04:00
Ryan Cramer
59549a01be Some optimizations in the PagesEditor class 2017-07-21 13:04:52 -04:00
Ryan Cramer
651e8bd20c CSS adjustment in AdminThemeDefault to improve appearance of language tabs in file/image fields. Also, remove an extra unnecessary margin on language-tabbed text Inputfields. 2017-07-17 06:08:47 -04:00
Ryan Cramer
b7cb72cac6 Fix issue with file/image tags where they weren't initiating properly after an AjaxUploadDone JS event 2017-07-17 05:47:43 -04:00
Ryan Cramer
faf1efc049 Adjust to WireFileTools::find() method to correct potential confusion between windows vs. unix directory separator 2017-07-16 09:57:31 -04:00
Ryan Cramer
0a06c12b82 A couple other tweaks to tags updates and bump version to 3.0.67 2017-07-14 14:09:46 -04:00
Ryan Cramer
659391a4c3 Fix issue processwire/processwire-issues#316 - legacy db API variable class and debug timers in PHP7 producing unnecessary notice 2017-07-14 10:39:54 -04:00
Ryan Cramer
0fe94521fe Fix issue processwire/processwire-issues#315 - remove some extra code that's no longer necessary for image fields in a PageTable 2017-07-14 10:15:31 -04:00
Ryan Cramer
bf351573b0 Fix issue processwire/processwire-issues#310 - update ProcessForgotPassword to use the same requirements as when editing the pass field in the page editor 2017-07-14 09:47:24 -04:00
Ryan Cramer
cab20d518d Add improved tags support for InputfieldFile and InputfieldImage. Now the UI is more tags oriented, and there are more config options for how the tags are input. This commit also adds a modified version of the Selectize js library to provide improved tag inputs. 2017-07-13 15:20:53 -04:00
Ryan Cramer
650fec9a37 Bump version to 3.0.66 2017-07-07 14:07:39 -04:00
Ryan Cramer
786995f85e Add new WireFileTools API var $files->find() method and update the include/compile methods in WireFileTools to use the TemplateFile render stack. 2017-07-07 14:06:28 -04:00
Ryan Cramer
b8d01e88d6 Add render stack support to TemplateFile so that a rendered php file can know what other file(s) are rendering it, when applicable. 2017-07-07 14:05:00 -04:00
Ryan Cramer
ae4761180f Some minor fixes to enable deleting a field that is missing its Fieldtype module. Plus addition of hasPage property to Inputfield modules. And a couple other minor things. 2017-07-07 14:03:39 -04:00
Ryan Cramer
5ec3e48de6 Fix issue processwire/processwire-issues#299 2017-06-30 10:49:31 -04:00
Ryan Cramer
bcd40cdf03 Bump version to 3.0.65 2017-06-23 15:50:18 -04:00
Ryan Cramer
5b0eb5ea81 Fix issue processwire/processwire-issues#264 2017-06-23 09:04:14 -04:00
Ryan Cramer
8107d5e90d Fix issue processwire/processwire-issues#276 (typo in translation text) 2017-06-23 07:34:17 -04:00
Ryan Cramer
651e0e8763 Fix issue processwire/processwire-issues#274 2017-06-23 07:32:03 -04:00
Ryan Cramer
0cccb763a5 Fix issue processwire/processwire-issues#281 (typo in phpdoc example) 2017-06-23 07:11:38 -04:00
Ryan Cramer
438a2944cb Upgrade CKEditor version from 4.6.2 to 4.7.0, plus fix issue processwire/processwire-issues#279 where image placement wasn't working correctly in CKE inline mode. 2017-06-23 06:51:53 -04:00
Ryan Cramer
da3c80d0ca Fix issue processwire/processwire-issues#275 - part 2, fix for exif.js 2017-06-23 06:22:10 -04:00
Ryan Cramer
7ad84bf1c3 Fix issue processwire/processwire-issues#275 2017-06-23 06:15:21 -04:00
Ryan Cramer
dbfb3e2c7e Fix issue processwire/processwire-issues#285 2017-06-22 11:29:35 -04:00
Ryan Cramer
763963ec39 Update WireMarkupRegions to remove class attribute-based action support, since our final spec uses action attributes instead. It only does this for PW installations installed today or later, just in case anyone is using some older examples on an existing site. If you want to force use of the newer version (which is more efficient) set $config->useMarkupRegions=2; The updates made in this commit might also fix processwire/processwire-issues#294 but have not yet confirmed. 2017-06-22 08:45:16 -04:00
Ryan Cramer
3348f3f13a Fix issue processwire/processwire-issues#286 2017-06-22 07:03:28 -04:00
Ryan Cramer
571266c6c1 Fix issue processwire/processwire-issues#288 2017-06-22 06:38:40 -04:00
Ryan Cramer
68a4ddee88 Fix issue processwire/processwire-issues#289 2017-06-21 10:12:09 -04:00
Ryan Cramer
b32a2f0058 Bump version to 3.0.64 2017-06-16 14:34:53 -04:00
Ryan Cramer
05b210766c Continued work on the PagesExportImport class. Not yet ready for use, but much further along and it works in testing on the API side for the basics. Wait before using it though, as there's much more to come here. 2017-06-16 12:50:07 -04:00
Ryan Cramer
0f55f41831 Correct minor issue in Lister for when both the view and edit links for individual pages are disabled 2017-06-16 12:49:19 -04:00
Ryan Cramer
e378acc7bc Correct issue with InputfieldText placeholder attribute sometimes not working for multi-language (like outside the page editor). 2017-06-16 05:56:58 -04:00
Ryan Cramer
1900675bbb Various minor fixes and bump version number to 3.0.63 2017-05-26 14:35:16 -04:00
Ryan Cramer
76fc6dada1 Fix issue processwire/processwire-issues#261 2017-05-26 09:44:14 -04:00
Ryan Cramer
1a590c586f Fix issue processwire/processwire-issues#267 2017-05-26 09:27:19 -04:00
Ryan Cramer
507555e907 Fix issue processwire/processwire-issues#268 2017-05-26 09:18:35 -04:00
Ryan Cramer
5f3827ecba Fix issue processwire/processwire-issues#272 2017-05-26 08:27:48 -04:00
Ryan Cramer
76097ea1ee Add client-side image resize support to InputfieldImage 2017-05-25 11:37:10 -04:00
Ryan Cramer
57b297fd1d Fix issue with InputfieldFile non-ajax mode not working for some cases and bump version to 3.0.62 2017-05-05 13:43:17 -04:00
Ryan Cramer
be99669203 Fix issue processwire/processwire-issues#255 2017-05-02 09:59:06 -04:00
Ryan Cramer
84a39c0667 Fix issue processwire/processwire-issues#256 where the image field modal upload function was not updating the parent window images field, thus preventing the image having the temporary status removed (and not being saved). 2017-05-02 09:50:45 -04:00
Ryan Cramer
b17c9eaed9 Fix InputfieldSelector issue identified by @renobird where checked OR-checkboxes in larger groups of same-field selectors could result in matching the OR to the wrong selector row. 2017-04-30 07:50:35 -04:00
Ryan Cramer
347240acd9 Minor adjustments to repeater and asmSelect 2017-04-28 14:54:26 -04:00
Ryan Cramer
7adf09e305 Small .htacess update for HTTPS redirect support on AWS ELB 2017-04-28 14:53:07 -04:00
Ryan Cramer
0cd8a7a276 Add WireHttp::getResponseHeaderValues() per processwire/processwire-issues#253 2017-04-28 09:46:08 -04:00
horst
64e9ca214d stripped out tab_width 2017-04-24 10:35:20 +02:00
horst
3337aff4d5 create .editorconfig in wire directory
This is very helpful for contributors who uses different editor settings, for example indentation with spaces, not tabs. Most of all common editor programms or IDEs nowadays support to preserve the defined coding style rules automatically. Read more here: http://editorconfig.org
2017-04-23 12:00:33 +02:00
horst
188d0e150d correct syntax highlighting of *.module files
on github. 
see: https://processwire.com/talk/topic/16124-github-now-support-custom-highlight-config/
2017-04-23 11:51:08 +02:00
Ryan Cramer
3fc9f69da7 Some minor adjustments and bump version to 3.0.61 2017-04-21 11:27:47 -04:00
Ryan Cramer
8d02e72320 Update font-awesome to 4.7 per processwire/processwire-requests#72 2017-04-20 05:59:28 -04:00
Ryan Cramer
1c46d0d44c Fix issue with repeaters in renderValue mode 2017-04-19 10:04:16 -04:00
Ryan Cramer
e83ed750c9 Add option for InputfieldHidden to still render as an input in renderValue mode 2017-04-19 10:03:30 -04:00
Ryan Cramer
c259ce8103 Fix minor issue with CommentForm front-end output 2017-04-19 10:02:00 -04:00
Ryan Cramer
a8febefa70 Fix issue processwire/processwire-issues#248 2017-04-19 09:33:43 -04:00
Ryan Cramer
42de2e7bda Fix issue processwire/processwire-issues#245 2017-04-19 09:29:50 -04:00
Ryan Cramer
bb12873a19 Fix issue with multi-template settings not always updating the selectable columns on columns tab 2017-04-16 06:46:31 -04:00
Ryan Cramer
93d1be8453 Bump version to 3.0.60 dev 2017-04-14 14:19:10 -04:00
Ryan Cramer
223b80d685 Update/add documentation to a few classes (like WireCache), correct issue with HTTPS detection on AWS load balancer, plus some other minor tweaks. 2017-04-14 06:32:23 -04:00
Ryan Cramer
93779e2017 Update MarkupHTMLPurifier version per processwire/processwire-issues#243 2017-04-14 06:29:11 -04:00
Ryan Cramer
fb1cc857f2 Fix issue processwire/processwire-issues#244 2017-04-14 06:12:44 -04:00
Ryan Cramer
b914586f6c Fix issue processwire/processwire-issues#227 update Sanitizer::selectorValue() allow exclamation point "!" at beginning of selector value 2017-04-13 08:54:49 -04:00
Ryan Cramer
fd2f14445c Fix issue processwire/processwire-issues#233 2017-04-13 08:36:23 -04:00
Ryan Cramer
267a368034 Fix issue processwire/processwire-issues#241 2017-04-13 06:14:45 -04:00
Ryan Cramer
ae50a0563b Attempt to fix issue processwire/processwire-issues#242 for FieldtypeComments + utf8mb4 charset combo 2017-04-13 06:06:46 -04:00
Ryan Cramer
81c8d4eb2f Fix issue processwire/processwire-issues#236 with InputfieldSelector, plus update the "None" selection option to fix another issue reported in ListerPro board 2017-04-10 08:23:45 -04:00
Ryan Cramer
168a4ffa58 Bump version to 3.0.59 plus a couple other minor updates 2017-04-07 10:14:40 -04:00
Ryan Cramer
00e7a46434 Fix issue processwire/processwire-issues#222 with InputfieldImage thumbnail logic 2017-04-07 06:08:50 -04:00
Ryan Cramer
0714279ba9 Update phpdoc for $pages->sort() per processwire/processwire-issues#225 2017-04-07 06:00:11 -04:00
Ryan Cramer
60989f97b4 Fix issue processwire/processwire-issues#229 2017-04-07 05:44:08 -04:00
Ryan Cramer
75a969bafb Various minor tweaks/updates 2017-04-07 05:11:06 -04:00
Ryan Cramer
98594eb024 Bump version to 3.0.58 2017-03-31 15:43:56 -04:00
Ryan Cramer
ef2fd54e68 Add spelling correction per @mestaritonttu PR #62 2017-03-30 07:01:51 -04:00
Ryan Cramer
2d1864c80d Add @jofalk PR #59 that fixes the issue with </edit> tags not being stripped from front-end editor. 2017-03-30 06:57:21 -04:00
Ryan Cramer
c5033e1e42 Add @derixithy PR #58 for additional sessionCookieDomain call 2017-03-30 06:48:02 -04:00
Ryan Cramer
3f67722294 Add @LostKobrakai PR #50 plus some other tweaks to FieldtypeDatetime module 2017-03-30 06:27:52 -04:00
Ryan Cramer
94876a7bde Merge branch 'Notanotherdotcom-master' into dev 2017-03-30 06:03:39 -04:00
Ryan Cramer
dd777ea9d8 Merge branch 'master' of https://github.com/Notanotherdotcom/processwire-1 into Notanotherdotcom-master 2017-03-30 06:03:16 -04:00
Ryan Cramer
2b51c75cee Various minor tweaks related to AdminThemeFramework 2017-03-30 05:53:46 -04:00
Ryan Cramer
4ca684df83 Fix 2 Lister related issues with 'parent' filters/columns identified by @somatonic in ListerPro board. 2017-03-26 06:40:35 -04:00
Ryan Cramer
ec4726b3df A few updates in support of new AdminThemeFramework and bump version to 3.0.57 2017-03-24 13:23:36 -04:00
Notanotherdotcom
04c87f1954 Branding Upgrade
Replaces the logo in the Default and Reno admin themes with the new
version for branding consistency.
2017-03-23 22:57:18 +00:00
Ryan Cramer
722b504273 Additional updates to $languages setLocale/getLocale methods per processwire/processwire-issues#215 2017-03-20 08:26:21 -04:00
Ryan Cramer
8e241f0132 Add @horst-n fix for ImageSizerEngine rounding issue processwire/processwire-issues#191 2017-03-20 05:49:34 -04:00
Ryan Cramer
a410bf8236 Some fixes to InputfieldSelector parent.subfield properties, plus fix issue where datetime field didn't always recognize when the date picker should include a time picker as well. 2017-03-19 07:07:52 -04:00
Ryan Cramer
1f23b54f45 Fix issue where non-default date format could get lost in session values of InputfieldSelector.module 2017-03-19 06:27:20 -04:00
Ryan Cramer
4414a2db2b Bump version to 3.0.56 plus some other minor tweaks 2017-03-17 13:25:29 -04:00
Ryan Cramer
9c92ce5305 Updates for issue processwire/processwire-issues#215 to better support locale settings on front-end, plus add $languages->setLocale() and $languages->getLocale() methods, and freshen up code and docs in related classes. 2017-03-16 09:49:47 -04:00
Ryan Cramer
e1928c9e3c Some cleanup in ProcessPageList.module, plus attempt compromise fix identified for ProcessPageEdit breadcrumb starting from issue #22. 2017-03-15 14:12:27 -04:00
Ryan Cramer
770c717baa Updates related to WireMarkupRegions discussion in issue processwire/processwire-issues#195 - fix behavior of boolean pw-before/pw-after attributes, and add support for <pw-region> or <region> tags. 2017-03-15 14:04:49 -04:00
Ryan Cramer
11ebcfb456 Fix issue where some non-text based FieldtypeTable subfields had selection issues in InputfieldSelector 2017-03-12 11:16:26 -04:00
Ryan Cramer
11c49f2bb1 Fix issue with page edit dropdown menu actions appearing in buttons they aren't intended to 2017-03-12 07:15:40 -04:00
Ryan Cramer
8018ecc7af Bump version to 3.0.55 and a couple other minor adjustments 2017-03-10 12:46:01 -05:00
Ryan Cramer
8b96e6b060 Attempt fix for issue processwire/processwire-issues#206 fix for multi-site config not working 2017-03-10 09:05:08 -05:00
Ryan Cramer
b899bc42e7 Fix issue processwire/processwire-issues#202 where the leave confirm box was appearing when it shouldn't, after saving after a file had been uploaded. Also added drag/drop protection so that if you accidentially drag/drop a file outside of the specified dropzone, it gets ignored, rather than loading the file in the browser. 2017-03-10 08:38:16 -05:00
Ryan Cramer
6fe703f699 Fix issue processwire/processwire-issues#203 update to make API-created users always have guest role 2017-03-10 08:13:56 -05:00
Ryan Cramer
12767e284d Fix issue processwire/processwire-issues#201 where InputfieldPassword wasn't honoring the minlength setting and was always requiring at least 8 characters 2017-03-10 07:55:18 -05:00
Ryan Cramer
9df4469314 Fix issue processwire/processwire-issues#200 where the WireMarkupRegions self-closing tags list needed <link> and a few others 2017-03-10 06:08:12 -05:00
Ryan Cramer
ed0ba504bc Fix issue processwire/processwire-issues#199 add clarification notes to InputfieldFile "Inputfield Type" option, and prevent selection of "Image" type when in a FieldtypeFile 2017-03-10 05:50:18 -05:00
Ryan Cramer
020c1beca5 Fix issue processwire/processwire-issues#197 correcting an issue with repeater item label contained string "{images.count}" 2017-03-09 15:12:26 -05:00
Ryan Cramer
97f33e02ed Fix issue processwire/processwire-issues#198 (typo misspelling in phpdoc) 2017-03-09 10:16:05 -05:00
Ryan Cramer
95f5cba2ed Fix issue processwire/processwire-issues#196 (cosmetics) 2017-03-09 10:08:54 -05:00
Ryan Cramer
a3ba477346 Fix issue processwire/processwire-issues#194 2017-03-09 09:48:32 -05:00
Ryan Cramer
e87dcd5985 Fix issue processwire/processwire-issues#192 where inserted emoji could cause text to be truncated on systems using dbEngine "utf8" (as opposed to "utf8mb4"). Because the emoji/MB4 detection and replacement has some overhead, it's not enabled by default. To enable, set $config->dbStripMB4=true; in your /site/config.php file. 2017-03-09 09:11:30 -05:00
Ryan Cramer
25bfb8a5a6 Fix issue processwire/processwire-issues#208 where JqueryWireTabs was not properly remembering current tab between requests 2017-03-09 05:44:14 -05:00
Ryan Cramer
bbb8e987c9 Add support for Pages > Tree navigation: drill down through the page tree within the top navigation dropdowns. This works in AdminThemeDefault and AdminThemeUikit but not in AdminThemeReno (which just shows the first level, per its nav setup). 2017-03-08 11:43:58 -05:00
Ryan Cramer
c4ea3c3356 Fix issue processwire/processwire-issues#189 FieldtypeOptions and "!=" operators 2017-03-07 10:31:13 -05:00
Ryan Cramer
f32593adfa Fix issue processwire/processwire-issues#188 to provide more detailed warning message when installing module that doesn't meet requirements 2017-03-07 09:03:58 -05:00
Ryan Cramer
d7d392fe95 Fix issue #187 where $page->httpUrl didn't respect Template::slashUrls==0 setting when used in non-multi-language. 2017-03-07 08:49:28 -05:00
Ryan Cramer
bc032e1ce3 Minor optimization to a few Page hook method calls in Fieldtype 2017-03-07 08:32:32 -05:00
Ryan Cramer
4d6ce48252 Update login locale detection to suggest different actions depending on whether multi-language support is installed, per processwire/processwire-issues#184 2017-03-07 08:24:04 -05:00
Ryan Cramer
c7d4e5f662 Fix issue processwire/processwire-issues#185 by adding a strict mode to the date sanitizer and updating the method notes a bit. 2017-03-07 07:54:37 -05:00
Ryan Cramer
d5971733f2 Tweak to InputfieldDatetime datepicker button option, plus adjustment to ProcessPageEdit when in modal field edit 2017-03-06 08:43:25 -05:00
Ryan Cramer
a12cb029bb Various updates supporting core admin theme customization and bump version to 3.0.54 2017-03-03 10:37:33 -05:00
Ryan Cramer
f137dd2966 Updated several modules and classes to better support admin theme customization, consistent with needs in AdminThemeUikit 2017-02-24 12:23:57 -05:00
Ryan Cramer
ad90996c0b Update CKEditor version to 4.6.2 2017-02-13 06:05:42 -05:00
Ryan Cramer
ceae45cda7 Add problematic basenaem/locale detection and warning per issue processwire/processwire-issues#157 2017-02-06 06:38:59 -05:00
Ryan Cramer
310ea9d281 Bump version to 3.0.52 2017-02-03 11:26:05 -05:00
Ryan Cramer
4fa5641256 Fix issue processwire/processwire-issues#182 2017-02-03 06:02:36 -05:00
Ryan Cramer
60d418fe12 Fix issue processwire/processwire-issues#117 2017-02-02 10:07:01 -05:00
Ryan Cramer
21f48868a1 Fix issue processwire/processwire-issues#178 2017-02-02 09:40:48 -05:00
Ryan Cramer
8305215407 Fix issue processwire/processwire-issues#175 2017-02-02 09:13:22 -05:00
Ryan Cramer
666ddc528b Fix issue processwire/processwire-issues#181 2017-02-02 05:56:44 -05:00
Ryan Cramer
e518650515 Fix issue processwire/processwire-issues#171 which updates our reserved field names list 2017-02-01 09:37:24 -05:00
Ryan Cramer
1f021206d1 Attempt to fix issue processwire/processwire-issues#168 2017-02-01 09:21:42 -05:00
Ryan Cramer
8e08e0d5ac Fix issue processwire/processwire-issues#163 2017-02-01 08:28:24 -05:00
Ryan Cramer
144bd01619 Fix issue processwire/processwire-issues#153 where selector fails when there is leading comma in quoted value 2017-02-01 06:36:40 -05:00
Ryan Cramer
81adf00b8f Some optimizations to WireMarkupRegions class, and add a hasField property to Inputfield modules when paired with a Fieldtype (similar to existing hasFieldtype property, except is a Field object rather than Fieldtype object). 2017-02-01 06:11:15 -05:00
Ryan Cramer
1886f132b5 Fix for debug mode warning in WireMarkupRegions plus other minor tweaks 2017-01-29 07:08:18 -05:00
Ryan Cramer
933d9de724 Minor admin css adjustment and bump version number to 3.0.51 2017-01-27 14:08:27 -05:00
Ryan Cramer
4a3812efe0 Add a "Publish and Add Another" option to the Submit button options in ProcessPageAdd.module (for when template contains only a title) 2017-01-27 14:05:44 -05:00
Ryan Cramer
01f4419113 Some adjustments to WireMarkupRegions 2017-01-27 14:04:31 -05:00
Ryan Cramer
2becc0cbba Add an val() method to the Inputfield class for consistency with jQuery. This method is a shortcut for getting or setting the value attribute of Inputfield objects 2017-01-27 14:03:28 -05:00
Ryan Cramer
279f65ec65 Small adjustment to InputfieldSelector to prevent access control scenario that hides selectable options for FieldtypePage/InputfieldPage fields. 2017-01-22 08:10:48 -05:00
Ryan Cramer
b50b3aa3c7 Update MarkupFieldtype class to detect FieldtypeLanguageInterface objects 2017-01-22 07:18:10 -05:00
Ryan Cramer
f01ad1fe25 Bump version to 3.0.50 2017-01-20 15:25:45 -05:00
Ryan Cramer
31efae4d16 Fix issue processwire/processwire-issues#166 2017-01-20 11:22:29 -05:00
Ryan Cramer
5a8a4af23f Updates to WireMarkupRegions class to support use of "pw-id" and "data-pw-id" as an alternative to "id", and support of "pw-[action]=id" and "data-pw-[action]=id" attributes as an alternative to the "pw-" class names introduced last week. The [action] can be: append, prepend, replace, remove, before, after. 2017-01-20 11:08:38 -05:00
Ryan Cramer
65eb8da0ff Various minor updates 2017-01-20 10:53:57 -05:00
Ryan Cramer
0d82cee465 Fix issue processwire/processwire-issues#164 2017-01-20 10:16:36 -05:00
Ryan Cramer
07dbc4e6f6 Fix issue #161 where deleted page using default file/image fallback could delete fallback file if deleting page without outputFormatting enabled. 2017-01-20 09:49:19 -05:00
Ryan Cramer
a434f6193d Bump version to 3.0.49 and adjustment to debug info generated in WireMarkupRegions.php 2017-01-13 16:52:26 -05:00
Ryan Cramer
062ebd6663 Move some code from PageRender.module to WireMarkupRegions.php and make additional updates to WireMarkupRegions, adding support for nested/recursive regions. 2017-01-13 12:23:16 -05:00
Ryan Cramer
12a45994bf Add support for markup regions, a simple new template file output strategy that bridges the gap between direct and delayed output. See comments in the PageRender.module file for more details. 2017-01-12 15:45:07 -05:00
Ryan Cramer
6bfe4e65e2 Minor corrections to phpdoc in Page, WireArray and PageArray 2017-01-12 15:09:22 -05:00
Ryan Cramer
501a097e29 Minor improvements to installer, mostly phpdoc related 2017-01-12 15:08:28 -05:00
Ryan Cramer
307d7c9e7d Adjustment to Page::isChanged() method to reduce Pages::save() overhead 2017-01-09 08:12:42 -05:00
Ryan Cramer
43986173fb Fix issue #151, minor correction to FileCompiler affecting single lines that contain both className and function from ProcessWire namespace at the same time 2017-01-08 06:52:13 -05:00
Ryan Cramer
5fbbd9e2c6 Bump version to 3.0.48 2017-01-06 16:25:29 -05:00
Ryan Cramer
cde8b0a3e3 Add some initial stuff to support pages export/import. Nothing to use here yet, just building the foundation. 2017-01-06 12:25:24 -05:00
Ryan Cramer
afd0e8bc95 Adjustment to FileCompiler to improve detection of false positive end-of-PHP blocks 2017-01-06 11:16:26 -05:00
Ryan Cramer
8d97815110 Minor correction to previous commit 2017-01-05 15:36:15 -05:00
Ryan Cramer
d4d90a53fe Add @rolandtoth PR #16 that triggers showInputfield or hideInputfield events when showing/hiding Inputfields due to dependencies. 2017-01-05 14:46:42 -05:00
Ryan Cramer
d785fd7523 Minor adjustments to comment blocks: Add @LostKobrakai PR #42, PR #44, PR #45 and add @owzim PR #43 2017-01-05 14:32:09 -05:00
Ryan Cramer
928a399b8a Add @gmclelland PR #47 that fixes "search autocomplete keyboard focus errors". This also adds a compromise of accommodates PR #48 which retains support of up-arrow-to-close only if down arrow hasn't already been pressed. See also processwire/processwire-issues#125 and processwire/processwire-issues#141 2017-01-05 14:14:29 -05:00
Ryan Cramer
9ed0e415ed Adjustment to yesterday's FileCompiler updates that corrects a case where wire() function calls are used within include() statements 2017-01-05 13:02:12 -05:00
Ryan Cramer
c0ef1aea92 Fix issue processwire/processwire-issues#98 and also make some upgrades/optimizations to FileCompiler 2017-01-04 13:06:03 -05:00
Ryan Cramer
506187d871 Merge branch 'adrianbj-fix-input-url-path' into dev 2017-01-03 05:17:04 -05:00
Ryan Cramer
34ee4e051c Adjustment to PR #46 2017-01-03 05:16:32 -05:00
Ryan Cramer
5238b00250 Merge branch 'fix-input-url-path' of https://github.com/adrianbj/processwire into adrianbj-fix-input-url-path 2017-01-03 05:14:16 -05:00
Ryan Cramer
0d8cce638c Minor adjustments 2017-01-03 05:13:21 -05:00
Ryan Cramer
51bda526eb Bump version to 3.0.47, fix issue #139, issue #123, and issue #115 2016-12-30 13:09:07 -05:00
Ryan Cramer
8cb944cf52 Some updates to the template_ids support for FieldtypePage/InputfieldPage 2016-12-30 07:22:51 -05:00
Ryan Cramer
7d57fc5784 Fix issue processwire/processwire-issues#135 2016-12-29 10:48:07 -05:00
Ryan Cramer
c2cf538317 Minor CKEditor README.md update related to last commit 2016-12-29 10:06:24 -05:00
Ryan Cramer
264c5b0b54 Fix issue processwire/processwire-issues#133 updating a few links pointing to old GitHub repo to point to new one 2016-12-29 10:03:28 -05:00
Ryan Cramer
4402932ee6 Add request issue processwire/processwire-issues#137 2016-12-29 09:55:43 -05:00
Ryan Cramer
88e143126f Fix issue processwire/processwire-issues#138 where PageFinder had a pagination issue introduced in 3.0.46 2016-12-28 13:55:52 -05:00
Ryan Cramer
e12095e622 Attempt to fix issue processwire/processwire-issues#134 2016-12-28 07:03:17 -05:00
Ryan Cramer
20b6ebb81f Fix issue #128 2016-12-26 11:10:09 -05:00
Ryan Cramer
b6ba7049a0 Fix issue #127 2016-12-26 10:35:04 -05:00
Ryan Cramer
4e2a3c1bac Fix issue that was preventing non-local property hooks from working in WireHooks class 2016-12-26 09:31:42 -05:00
Ryan Cramer
622896e028 Fix issues #130 and #131 (dup) 2016-12-25 07:44:55 -05:00
Ryan Cramer
e8b9f12943 Bump version to 3.0.46 2016-12-23 15:13:13 -05:00
Ryan Cramer
ba9688af65 Improvements to dropdown submit buttons so that the menu doesn't disappear immediately when accidentally outside of it. 2016-12-23 12:24:52 -05:00
Ryan Cramer
e14d52f722 Add new $pages API methods: findIDs(), sort(), insertAfter, insertBefore(). Add support for negative "limit" and "start" values in selectors, where negative values reference the end of the set rather than the beginning. Add support for "eq=n" (or alias "index=n") selectors (both WireArray and PageFinder) for pulling a specific n'th item, can also specify "first" or "last" for "n". Update $page->editUrl(true) method to force return of edit URL with scheme and hostname. 2016-12-23 12:18:58 -05:00
adrianbj
137cbbd186 Fix for incorrect path returned from $input->url() when called before page is available.
It used to return: 
//admin/page/edit/-id-1111/?id=1111
Instead of the correct:
/admin/page/edit/?id=1111

This appears to fix everything correctly.
2016-12-23 08:38:20 -08:00
Ryan Cramer
0844bf2e47 Fix issue processwire/processwire-issues#119 2016-12-20 06:27:26 -05:00
Ryan Cramer
381293e0c7 Add a new $pages->findIDs() method, plus some experimental options in PageFinder 2016-12-19 13:38:07 -05:00
Ryan Cramer
e34918c73a Attempt to fix processwire/processwire-issues#122 2016-12-19 13:00:20 -05:00
Ryan Cramer
2570d6c86d Fix issue processwire/processwire-issues#121 2016-12-19 12:34:59 -05:00
Ryan Cramer
315251fa04 Some optimizations to PagesLoader::find() and findShortcut() methods. 2016-12-19 07:00:25 -05:00
Ryan Cramer
aab97a0b00 Fix inadvertent debug mode memory leak issue when using $pages->findMany(), plus optimize PageTable for faster load when sortfield(s) are in use. Per @apeisa / Avoine. 2016-12-19 06:57:37 -05:00
Ryan Cramer
3f758312a6 Bump version to 3.0.45 2016-12-16 15:19:38 -05:00
Ryan Cramer
8d8d9dfe3d Update the PageArrayIterator::$chunkSize setting to be configurable via $config->lazyPageChunkSize per @apeisa 2016-12-16 11:12:23 -05:00
Ryan Cramer
ffde966920 Upgrades to configuration of FieldtypePage and InputfieldPage (as seen in field editor), Upgrades to Page and WireArray __invoke() support, Rewrite Selectors::stringHasSelector() method to make it more accurate, Upgrades to Sanitizer::minArray(), add support for load-time filtering to FieldtypePage (Page fields), plus some other small tweaks. 2016-12-16 10:45:32 -05:00
Ryan Cramer
83846d1fe2 Minor tweaks 2016-12-14 12:26:26 -05:00
Ryan Cramer
7e262fa60d Some updates to the wireRegion()/region() function to support locking prepend or append 2016-12-14 11:36:26 -05:00
Ryan Cramer
2b9a7adbcb Update to allow for Page objects to load pages (children, parent, etc.) that don't get cached when their load option specified cache=false. Also makes the 'parent' page lazy loading for Page objects, so that it doesn't load the parent Page until a $page->parent() call is requested. Updates for @apeisa / Avoine request. 2016-12-14 11:02:32 -05:00
Ryan Cramer
3821dc2be3 Add add inline script support to Inputfield elements rendered with Lister per processwire/processwire-issues#112 2016-12-14 06:00:38 -05:00
Ryan Cramer
d077dfa32b Update InputfieldRepeater to process inline scripts from Inputfields per processwire/processwire-issues#112 2016-12-14 05:47:16 -05:00
Ryan Cramer
953ca72014 Various updates including optimizations to WireHooks, support for a hookable renderReadyHook method in Inputfield, cache prevention measures for Pages::findMany() per @apeisa, and some work in progress on InputfieldSelector support for improved "custom (field=value)" searches. 2016-12-13 14:24:01 -05:00
Ryan Cramer
e2e8c35c2c Fix issue processwire/processwire-issues#110 2016-12-12 09:26:09 -05:00
Ryan Cramer
f0212dcc9c Updates to support multiple template selections for Page fields per @apeisa and @niklam PR #22, plus bump version to 3.0.44 2016-12-09 14:16:04 -05:00
Ryan Cramer
880810c6bb Additional repeater updates including addition of a "minimum items" option, and support for an accordion mode. 2016-12-09 10:08:55 -05:00
Ryan Cramer
6027e87a5e Repeater updates continued with a complete refactoring of the InputfieldRepeater.js file. This update also corrects the depth-dragging issues that were present in the last commit (issues were especially noticable dragging when using AdminThemeReno). Also adds a repeater open all/close all function per processwire/processwire-requests#33 - to use it, double click the "on/off" toggle icon that appears next to the trash icon. 2016-12-08 14:02:43 -05:00
Ryan Cramer
d92674fd4a Minor adjustment to improve repeater depth drag behavior in AdminThemeReno. Still not there yet, but this adjustment improves it a little bit. Works much better in AdminThemeDefault for some reason. 2016-12-07 15:24:42 -05:00
Ryan Cramer
c44329817c Correct issue where AdminThemeReno was missing the extra "head" output, which prevented the vex dialogs from being styled 2016-12-07 12:29:05 -05:00
Ryan Cramer
d1dcebe002 Update cachebuster version code in admin themes, just in case the previous commits updates to main.js are cached and not immediately visible 2016-12-07 12:21:47 -05:00
Ryan Cramer
e2f9597c5a Update Repeaters to add these features: 1) Support for repeater depth per @jlahijani, 2) Add new repeater item "clone" feature, 3) Add support for max items, 4) Add double-click trash icon to "delete all" feature. This commit also adds the Vex library for nicer looking Javascript alert and confirm boxes, which we use to confirm "clone" and "delete all" operations in repeaters, and will use elsewhere. 2016-12-07 11:24:49 -05:00
Ryan Cramer
423fbe6f57 Fix issue in Page.php formatFieldValue function that occurs when formatting a single image|null field. 2016-12-03 07:55:08 -05:00
Ryan Cramer
ac9487e967 Bump version to 3.0.43 2016-12-02 13:31:09 -05:00
Ryan Cramer
af5cda1d6d Minor adjustment per remaining button in issue in processwire/processwire-issues#84 2016-12-02 11:23:24 -05:00
Ryan Cramer
707cd9e735 Expand upon the Page::url() method to include an $options argument that adds many new capabilities. 2016-12-02 10:38:49 -05:00
Ryan Cramer
7eddb51bc9 Some documentation adjustments to WireArray 2016-12-01 12:36:53 -05:00
Ryan Cramer
015725191b Fix issue processwire/processwire-issues#100 where WordPress creating a fake mb_strlen() function made PW think that multibyte support was installed. 2016-12-01 12:07:25 -05:00
Ryan Cramer
9cdf68e6b8 Fix processwire/processwire-issues#99 where multi-instance URLs that had hostname specified were missing a leading slash 2016-12-01 12:00:41 -05:00
Ryan Cramer
cbdf8e9063 Fix issue processwire/processwire-issues#97 where using {images.count} in repeater label caused issue with loading images in the repeater 2016-12-01 11:22:40 -05:00
Ryan Cramer
ef42513831 Add PageFieldValueInterface as an implementation option for objects supporting page field values. Basically a helper for certain object-based values like Pagefiles/Pageimages, and likely others. 2016-12-01 10:56:04 -05:00
Ryan Cramer
30f96593bb Fix issue processwire/processwire-issues#94 for integer field not adding min attribute to HTML5 number type when 0 specified as minimum and no maximum specified. 2016-12-01 05:40:24 -05:00
Ryan Cramer
b8e51db176 Some updates to touch support per issue #84, plus a few other minor things that got bundled in 2016-11-30 13:18:14 -05:00
Ryan Cramer
6a3789d892 Merge branch 'marcus-herrmann-fix/improve-a11y-markup-pager-nav' into dev 2016-11-29 10:39:50 -05:00
Ryan Cramer
c4958461f1 Some changes to Marcus's PR, plus additional updates to MarkupPagerNav module, including expanded documentation 2016-11-29 10:38:26 -05:00
Ryan Cramer
103642ef6d Merge branch 'fix/improve-a11y-markup-pager-nav' of https://github.com/marcus-herrmann/processwire-1 into marcus-herrmann-fix/improve-a11y-markup-pager-nav 2016-11-29 06:06:06 -05:00
Ryan Cramer
76cb88815a Merge branch 'teppokoivula-feature-comments-manager-tweaks' into dev 2016-11-29 06:00:50 -05:00
Ryan Cramer
d3f9d4fd5b Merge branch 'feature-comments-manager-tweaks' of https://github.com/teppokoivula/processwire-1 into teppokoivula-feature-comments-manager-tweaks 2016-11-29 06:00:34 -05:00
Ryan Cramer
53612e9489 Update ProcessField to use more verbose field type names for additional clarity. Also a couple minor cosmetic adjustments in LanguageSupport 2016-11-28 11:04:02 -05:00
Ryan Cramer
bba6e3b00f Add option to exclude FileCompiler at include() time by specifying include(/*NoCompile*/'filename.php'); or include('filename.php'/*NoCompile*/);. The include() can also be include_once(), require(), require_once(), wireRenderFile(), wireIncludeFile() or new TemplateFile(). Relevent to PR #13 2016-11-28 10:59:33 -05:00
mhe
bf2ff863dd Update/simplify MarkupPagerNav default markup 2016-11-27 17:12:01 +01:00
mhe
016290192f Use aria-current also in built in site profiles 2016-11-27 17:10:22 +01:00
mhe
7038b787f0 Add defaults, labels, markers for accessibility to MarkupPagerNav 2016-11-27 10:47:25 +01:00
teppokoivula
a8efd4c04b Fix typo and make tab navigation always visible
This commit fixes a typo that prevented using the module and also makes
tab navigation visible even when active tab is empty.

Fixes processwire/processwire-issues#95
2016-11-26 20:42:45 +02:00
Ryan Cramer
332dbb317e Merge branch 'marcus-herrmann-fix/a11y-core-site-profiles' into dev 2016-11-25 15:04:41 -05:00
Ryan Cramer
cfb6823d49 Merge branch 'fix/a11y-core-site-profiles' of https://github.com/marcus-herrmann/processwire-1 into marcus-herrmann-fix/a11y-core-site-profiles 2016-11-25 15:03:34 -05:00
Ryan Cramer
35df716082 Additional documentation updates and bump version to 3.0.42 2016-11-25 14:58:02 -05:00
Ryan Cramer
942cac2707 Update $sanitizer->testAll() method for more methods per processwire/processwire-issues#85 2016-11-23 07:15:17 -05:00
Ryan Cramer
2a66b67174 Fix issue processwire/processwire-issues#92 where $sanitizer->markupToText() left useless trailing character when replacing br tags 2016-11-23 06:43:14 -05:00
Ryan Cramer
c1f4693ca0 Documentation updates to several core classes 2016-11-22 14:26:41 -05:00
Ryan Cramer
3647a47b86 Adjustment to ProcessPageEditLink.module to correct debug mode warnings that appear when called from inside a CKEditor field that's in a repeater 2016-11-20 06:48:52 -05:00
mhe
28d119c273 Improve accessibility in site profiles 2016-11-20 09:47:25 +01:00
Ryan Cramer
d013ef8550 Merge branch 'derixithy-dev' into dev 2016-11-18 12:29:48 -05:00
Ryan Cramer
6fbfdbab95 Merge branch 'dev' of https://github.com/derixithy/processwire into derixithy-dev 2016-11-18 12:28:39 -05:00
Ryan Cramer
d935e9b699 Some minor doc tweaks, update $modules->getModuleInfo() method to support returning info for all modules, bump version to 3.0.41 2016-11-18 12:23:27 -05:00
Ryan Cramer
a1219e38e9 Some phpdoc updates in a few classes, plus fix a bug in the $config->paths() method 2016-11-17 15:20:09 -05:00
Ryan Cramer
ab7b7a6380 Fix issue processwire/processwire-issues#80 where missing site/templates/admin.php file rendered blank output rather than throwing exception 2016-11-15 08:03:55 -05:00
Ryan Cramer
a98cd1b351 Merge branch 'borantula-patch-1' into dev 2016-11-14 08:12:37 -05:00
Ryan Cramer
8b1e5a3c41 Fix issue processwire/processwire-issues#66 where deleting item from 2 different repeater fields at same time resulted in only item from first repeater being deleted 2016-11-14 07:57:29 -05:00
Ryan Cramer
facc671e8d Fix issue processwire/processwire-issues#81 where 2 translation phrases were on 1 line when they should have been on 2 2016-11-14 06:42:35 -05:00
Ryan Cramer
4b800adb5a Fix issue #78 with PagePathHistory where a cloned child of a renamed parent page could redirect to wrong item when accessed at old URL 2016-11-14 06:38:11 -05:00
Ryan Cramer
e0af12d7ab Fix issue #76 with typo in Pages.php 2016-11-14 06:21:36 -05:00
Ryan Cramer
58c659b288 Add @adrianbj PR #34 removing duplicate line in ProcessTemplate.module 2016-11-14 06:13:56 -05:00
Ryan Cramer
7700627d33 Merge branch 'pine3ree-patch-1' into dev 2016-11-14 05:57:57 -05:00
Ryan Cramer
0d81ff542b Merge branch 'patch-1' of https://github.com/pine3ree/processwire-1 into pine3ree-patch-1 2016-11-14 05:57:39 -05:00
Ryan Cramer
25c703b924 Merge branch 'teppokoivula-feature-querystring-overrides' into dev 2016-11-14 05:51:05 -05:00
Ryan Cramer
a35d00baab Merge branch 'feature-querystring-overrides' of https://github.com/teppokoivula/processwire-1 into teppokoivula-feature-querystring-overrides 2016-11-14 05:50:41 -05:00
Ryan Cramer
654c18fbe9 Various minor tweaks and documentation improvements 2016-11-14 05:37:50 -05:00
Bora Yalçın
1bb92f1941 turkish chars added to defaultReplacements array
Special letters in Turkish alphabet (see below) added to default replacements array as they were becoming - in page names.

ı=i
ğ=g
İ=i
Ç=c
ş=s
Ş=s
2016-11-09 09:37:56 +01:00
Derixithy
3897df4436 Added support for cookie domain 2016-11-07 20:14:52 +01:00
maks feltrin
78acf909d3 Sanitizer::string small refactoring
Since we are doing type-checking we are dealing with mutually excluding conditions.

So far the code blocks for non-string type matching will transform `$value` into a string anyway so if we have an object, the null value or a bool we would get a string and any further test would be not needed for it. W only need to test the unprocessed value, i.e. other conditions on the input value.

Now the code reads like this:
object? => get a string
or null? => get an (empty) string
or bool? => get a (numeric|empty) string
or array? => build a string
or if anything else but a string => cast to string

Off-Topic:
shouldn't be better to use `null === $var` / `null !== $var` instead of calling `is_null`. Inside a function that can be called many times it can make a diifference in processing time since calling a function is more expensive, even though i agree to be more consistent with other type-checking.

kind regards
2016-11-07 18:17:43 +01:00
Ryan Cramer
2d9e959bf2 Update InputfieldSelector to support sorted by field labels (rather than names) and support for better multi-language subfield names. 2016-11-06 08:13:34 -05:00
Ryan Cramer
21bf57eb79 Add support for OR-groups in "custom (field=value)" selections in InputfieldSelector. Simply use the existing "or" checkbox to apply the custom selector as an OR group. 2016-11-06 07:23:36 -05:00
teppokoivula
0a12932951 Add support for optional overrides array to $input->queryString() 2016-11-05 14:53:09 +02:00
Ryan Cramer
8d0c8de3ee Bump version to 3.0.40 2016-11-04 11:45:14 -04:00
Ryan Cramer
8bf7b0dc41 Add @pine3ree PR #24 which adds a break statement to a for() loop in WireFileTools.php 2016-11-03 12:18:53 -04:00
Ryan Cramer
29023269a4 Merge branch 'pine3ree-patch-2' into dev 2016-11-03 12:16:05 -04:00
Ryan Cramer
a9a2ff35ac Merge branch 'patch-2' of https://github.com/pine3ree/processwire-1 into pine3ree-patch-2 2016-11-03 12:15:55 -04:00
Ryan Cramer
35639b0f11 Merge branch 'LostKobrakai-feature/min-size-by-aspect' into dev 2016-11-03 12:13:04 -04:00
Ryan Cramer
99008366ba Merge branch 'feature/min-size-by-aspect' of https://github.com/LostKobrakai/processwire-1 into LostKobrakai-feature/min-size-by-aspect 2016-11-03 12:12:44 -04:00
Ryan Cramer
8405c586f0 Add @iamwebrocker PR #18 which adds honeypot option to comments form 2016-11-03 11:58:06 -04:00
Ryan Cramer
9b385168d0 Spelling correction in MarkupRSS.module per @lesaff PR #26 2016-11-03 11:33:17 -04:00
Ryan Cramer
243998039f Add @clsource PR #23 to correct a phpdoc typo in Functions.php 2016-11-03 11:30:48 -04:00
Ryan Cramer
2f66734a6b Add @clsource PR #23 to correct a phpdoc typo in Functions.php 2016-11-03 11:30:03 -04:00
Ryan Cramer
c26095e9e3 Merge branch 'gmclelland-fix-issue-69' into dev 2016-11-03 11:20:25 -04:00
Ryan Cramer
8bc1b51e03 Merge branch 'fix-issue-69' of https://github.com/gmclelland/processwire into gmclelland-fix-issue-69 2016-11-03 11:19:23 -04:00
Ryan Cramer
0fb25668c4 Update SystemUpdater module to remove any session data associated with admin theme when a core update occurs 2016-11-03 11:11:40 -04:00
Ryan Cramer
2135f2ca2e Update AdminThemeReno to remove an extra pixel appearing underneath selected tab in WireTabs 2016-11-03 11:10:46 -04:00
Ryan Cramer
1633b990ca Update several Inputfield modules to remove unnecessary newlines in markup and add phpdocs where appropriate 2016-11-03 11:10:04 -04:00
Ryan Cramer
1c3ea5ce51 Fix issue processwire/processwire-issues#65 modal window on InputfieldForm with HTML5 required inputs 2016-11-03 09:54:58 -04:00
Ryan Cramer
8116a5ba77 Merge branch 'dev-css' into dev-dev 2016-11-02 13:09:31 -04:00
Glenn McLelland
2ddb3928bd Used the css function to display the element inline 2016-11-02 11:23:24 -05:00
Glenn McLelland
4eca3fd458 Revert "Added a css fix for issue #69" going to use a different technique.
This reverts commit 66149758d9.
2016-11-02 11:07:27 -05:00
Benjamin Milde
dc860bc929 Allow min and max dimensions for images to be swapped for portrait images 2016-11-02 12:39:30 +01:00
Glenn McLelland
66149758d9 Added a css fix for issue #69 2016-11-01 14:59:06 -05:00
Ryan Cramer
87ea1685b2 Update to convert several css selectors to be "pw-" selectors, as part of longer term goal to work nicely with other css frameworks. 2016-11-01 15:03:41 -04:00
maks feltrin
83d4788764 allow custom th classes
this allows customization of th elements and when using relative column sizing classes it also allows to avoid repeating those classes in every tbody row.
2016-11-01 02:32:08 +01:00
1289 changed files with 101667 additions and 13289 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
$ cat .gitattributes
*.module linguist-language=PHP

View File

@@ -410,7 +410,7 @@ https://processwire.com/about/license/mit/
The MIT License (MIT)
Copyright (c) 2015 Ryan Cramer <or other year/person if indicated in file>
Copyright (c) 2018 Ryan Cramer <or other year/person if indicated in file>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -140,5 +140,5 @@ resolved any issues.
------
Copyright 2016 by Ryan Cramer / Ryan Cramer Design, LLC
Copyright 2018 by Ryan Cramer / Ryan Cramer Design, LLC

View File

@@ -93,6 +93,10 @@ DirectoryIndex index.php index.html index.htm
# -----------------------------------------------------------------------------------------------
# RewriteCond %{HTTPS} off
# RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# If using an AWS load balancer, use these two lines below instead of those above:
# RewriteCond %{HTTP:X-Forwarded-Proto} =http
# RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# -----------------------------------------------------------------------------------------------
# 10. Set an environment variable so the installer can detect that mod_rewrite is active.
@@ -201,7 +205,10 @@ DirectoryIndex index.php index.html index.htm
# 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
# below if you don't need to use the $config->pagefileSecure option.
# below if you don't need to use the $config->pagefileSecure option. After uncommenting, test
# a URL like domain.com/site/assets/files/test.jpg to make sure you are getting a 404 and not
# your homepage. If getting your homepage, then either: do not use this option, or comment out
# section #2 above that makes ProcessWire the 404 handler.
# -----------------------------------------------------------------------------------------------
# RewriteCond %{REQUEST_FILENAME} !\.(jpg|jpeg|gif|png|ico)$ [NC]

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,8 @@ if(!defined("PROCESSWIRE")) die();
/*** SITE CONFIG *************************************************************************/
/** @var Config $config */
/**
* Enable debug mode?
*

View File

@@ -1,9 +1,9 @@
</div><!--/#main-->
</main>
<!-- footer -->
<footer id='footer'>
<footer id='footer' role="contentinfo">
<p>
Powered by <a href='http://processwire.com'>ProcessWire CMS</a> &nbsp; / &nbsp;
<?php

View File

@@ -22,7 +22,7 @@ function renderNav(PageArray $items) {
if(!$items->count()) return;
echo "<ul class='nav'>";
echo "<ul class='nav' role='navigation'>";
// cycle through all the items
foreach($items as $item) {
@@ -30,7 +30,7 @@ function renderNav(PageArray $items) {
// render markup for each navigation item as an <li>
if($item->id == wire('page')->id) {
// if current item is the same as the page being viewed, add a "current" class to it
echo "<li class='current'>";
echo "<li class='current' aria-current='true'>";
} else {
// otherwise just a regular list item
echo "<li>";
@@ -67,15 +67,16 @@ function renderNavTree($items, $maxDepth = 3) {
// $out is where we store the markup we are creating in this function
// start our <ul> markup
echo "<ul class='nav nav-tree'>";
echo "<ul class='nav nav-tree' role='navigation'>";
// cycle through all the items
foreach($items as $item) {
// markup for the list item...
// if current item is the same as the page being viewed, add a "current" class to it
// if current item is the same as the page being viewed, add a "current" class and
// visually hidden text for screen readers to it
if($item->id == wire('page')->id) {
echo "<li class='current'>";
echo "<li class='current' aria-current='true'><span class='visually-hidden'>Current page: </span>";
} else {
echo "<li>";
}

View File

@@ -11,7 +11,7 @@
<body class='has-sidebar'>
<!-- top navigation -->
<ul class='topnav'><?php
<ul class='topnav' role='navigation'><?php
// top navigation consists of homepage and its visible children
$homepage = $pages->get('/');
@@ -25,7 +25,7 @@
if($child->id == $page->rootParent->id) {
// this $child page is currently being viewed (or one of it's children/descendents)
// so we highlight it as the current page in the navigation
echo "<li class='current'><a href='$child->url'>$child->title</a></li>";
echo "<li class='current' aria-current='true'><span class='visually-hidden'>Current page: </span><a href='$child->url'>$child->title</a></li>";
} else {
echo "<li><a href='$child->url'>$child->title</a></li>";
}
@@ -40,12 +40,13 @@
<!-- search form -->
<form class='search' action='<?php echo $pages->get('template=search')->url; ?>' method='get'>
<input type='text' name='q' placeholder='Search' value='' />
<button type='submit' name='submit'>Search</button>
<label for='search' class='visually-hidden'>Search:</label>
<input type='text' name='q' id='search' placeholder='Search' value='' />
<button type='submit' name='submit' class='visually-hidden'>Search</button>
</form>
<!-- breadcrumbs -->
<div class='breadcrumbs'><?php
<div class='breadcrumbs' role='navigation' aria-label='You are here:'><?php
// breadcrumbs are the current page's parents
foreach($page->parents() as $item) {
@@ -56,5 +57,5 @@
?></div>
<div id='main'>
<main id='main'>

View File

@@ -23,7 +23,7 @@ include('./_head.php'); // include header markup ?>
?></div><!-- end content -->
<div id='sidebar'><?php
<aside id='sidebar'><?php
// rootParent is the parent page closest to the homepage
// you can think of this as the "section" that the user is in
@@ -40,6 +40,6 @@ include('./_head.php'); // include header markup ?>
// output sidebar text if the page has it
echo $page->sidebar;
?></div><!-- end sidebar -->
?></aside><!-- end sidebar -->
<?php include('./_foot.php'); // include footer markup ?>

View File

@@ -6,6 +6,7 @@
* 3. Main content and sidebar
* 4. Footer
* 5. Media queries for responsive layout
* 6. Accessibility helpers
*
*/
@@ -124,9 +125,6 @@ form.search {
border: 1px solid #ccc;
width: 100%;
}
form.search button {
display: none;
}
.breadcrumbs {
clear: both;
@@ -291,3 +289,43 @@ figure figcaption {
font-size: 115%;
}
}
/*********************************************************************
* 6. Accessibility helpers
*
*/
/* Hide visually, but remain approachable for screenreader */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
white-space: nowrap;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
border: 0;
}
/* Show bypass link on hover */
.element-focusable:focus {
clip: auto;
overflow: visible;
height: auto;
}
/* Sample styling for bypass link */
.bypass-to-main:focus {
top: 0;
left: 0;
width: 100%;
height: 40px;
line-height: 40px;
text-align: center;
background: #333;
color: #fff;
}

View File

@@ -26,6 +26,8 @@ if(!defined("PROCESSWIRE")) die();
/*** SITE CONFIG *************************************************************************/
/** @var Config $config */
/**
* Enable debug mode?
*

View File

@@ -26,6 +26,8 @@ if(!defined("PROCESSWIRE")) die();
/*** SITE CONFIG *************************************************************************/
/** @var Config $config */
/**
* Enable debug mode?
*

View File

@@ -6,7 +6,7 @@
if($page->numChildren) {
echo "<ul class='nav'>";
echo "<ul class='nav' role='navigation'>";
foreach($page->children as $child) {
echo "<li><p><a href='{$child->url}'>{$child->title}</a><br />";
@@ -24,7 +24,7 @@ if($page->numChildren) {
</div><!--/content-->
<div id="footer" class="footer">
<div id="footer" class="footer" role="contentinfo">
<div class="container">
<p>Powered by <a href='http://processwire.com'>ProcessWire Open Source CMS/CMF</a></p>
</div>

View File

@@ -32,6 +32,8 @@
</head>
<body>
<a href="#bodycopy" class="visually-hidden element-focusable bypass-to-main">Skip to content</a>
<p id='bgtitle'><?php
// print the section title as big faded text that appears near the top left of the page
@@ -45,11 +47,12 @@
<a href='<?php echo $config->urls->root; ?>'><p id='logo'>ProcessWire</p></a>
<ul id='topnav'><?php
<ul id='topnav' role='navigation'><?php
// Create the top navigation list by listing the children of the homepage.
// If the section we are in is the current (identified by $page->rootParent)
// then note it with <a class='on'> so we can style it differently in our CSS.
// then note it with <a class='on'> so we can style it differently in our CSS
// and add a text that is visually hidden, but available for screen readers.
// In this case we also want the homepage to be part of our top navigation,
// so we prepend it to the pages we cycle through:
@@ -58,19 +61,24 @@
$children->prepend($homepage);
foreach($children as $child) {
if ($child === $page->rootParent) {
$class = " class='on'";
$indicator = "<span class='visually-hidden'>Current page: </span>";
$ariaState = " aria-current='true' ";
}
$class = $child === $page->rootParent ? " class='on'" : '';
echo "<li><a$class href='{$child->url}'>{$child->title}</a></li>";
echo "<li><a$class$ariaState href='{$child->url}'>$indicator{$child->title}</a></li>";
}
?></ul>
<ul id='breadcrumb'><?php
<ul id='breadcrumb' role='navigation' aria-label='You are here:'><?php
// Create breadcrumb navigation by cycling through the current $page's
// parents in order, linking to each:
foreach($page->parents as $parent) {
echo "<li><a href='{$parent->url}'>{$parent->title}</a> &gt; </li>";
echo "<li><a href='{$parent->url}'>{$parent->title}</a> <span class='visually-hidden'>&gt;</span> </li>";
}
?></ul>
@@ -87,6 +95,7 @@
?></h1>
<form id='search_form' action='<?php echo $config->urls->root?>search/' method='get'>
<label for='search_query' class='visually-hidden'>Search:</label>
<input type='text' name='q' id='search_query' value='<?php echo htmlentities($input->whitelist('q'), ENT_QUOTES, 'UTF-8'); ?>' />
<button type='submit' id='search_submit'>Search</button>
</form>
@@ -111,7 +120,7 @@
<div class="container">
<div id="sidebar">
<div id="sidebar" role='complementary'>
<?php
@@ -128,11 +137,12 @@
// We have determined that we're not on the homepage
// and that this section has child pages, so make navigation:
echo "<ul id='subnav' class='nav'>";
echo "<ul id='subnav' class='nav' role='navigation'>";
foreach($page->rootParent->children as $child) {
$class = $page === $child ? " class='on'" : '';
echo "<li><a$class href='{$child->url}'>{$child->title}</a></li>";
$ariaState = $page === $child ? " aria-current='true' " : '';
echo "<li><a$class$ariaState href='{$child->url}'>{$child->title}</a></li>";
}
echo "</ul>";
@@ -156,5 +166,5 @@
</div><!--/sidebar-->
<div id="bodycopy">
<div id="bodycopy" role="main">

View File

@@ -502,3 +502,45 @@ body, input, textarea, table {
z-index: 9999;
}
/*********************************************************************
* 6. Accessibility helpers
*
*/
/* Hide visually, but remain approachable for screenreader */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
white-space: nowrap;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
border: 0;
}
/* Show bypass link on hover */
.element-focusable:focus {
clip: auto;
overflow: visible;
height: auto;
}
/* Sample styling for bypass link */
.bypass-to-main:focus {
top: 0;
left: 0;
z-index: 10;
width: 100%;
height: 40px;
line-height: 40px;
text-align: center;
background: #333;
color: #fff;
}

View File

@@ -26,6 +26,8 @@ if(!defined("PROCESSWIRE")) die();
/*** SITE CONFIG *************************************************************************/
/** @var Config $config */
/**
* Enable debug mode?
*

View File

@@ -27,7 +27,7 @@ class Helloworld extends WireData implements Module {
return array(
// The module'ss title, typically a little more descriptive than the class name
// The module's title, typically a little more descriptive than the class name
'title' => 'Hello World',
// version number

View File

@@ -69,9 +69,6 @@ learning module development:
There is a module development forum located at:
https://processwire.com/talk/forum/19-moduleplugin-development/
For a tutorial on how to create modules, see:
http://wiki.processwire.com/index.php/Module_Creation
Additional resources
--------------------

View File

@@ -41,12 +41,14 @@
</head>
<body class="<?php if($sidebar) echo "has-sidebar "; ?>">
<a href="#main" class="visually-hidden element-focusable bypass-to-main">Skip to content</a>
<!-- top navigation -->
<ul class='topnav'><?php
<ul class='topnav' role='navigation'><?php
// top navigation consists of homepage and its visible children
foreach($homepage->and($homepage->children) as $item) {
if($item->id == $page->rootParent->id) {
echo "<li class='current'>";
echo "<li class='current' aria-current='true'><span class='visually-hidden'>Current page: </span>";
} else {
echo "<li>";
}
@@ -59,12 +61,13 @@
<!-- search form-->
<form class='search' action='<?php echo $pages->get('template=search')->url; ?>' method='get'>
<input type='text' name='q' placeholder='Search' value='<?php echo $sanitizer->entities($input->whitelist('q')); ?>' />
<button type='submit' name='submit'>Search</button>
<label for='search' class='visually-hidden'>Search:</label>
<input type='text' name='q' placeholder='Search' id='search' value='<?php echo $sanitizer->entities($input->whitelist('q')); ?>' />
<button type='submit' name='submit' class='visually-hidden'>Search</button>
</form>
<!-- breadcrumbs -->
<div class='breadcrumbs'><?php
<div class='breadcrumbs' role='navigation' aria-label='You are here:'><?php
// breadcrumbs are the current page's parents
foreach($page->parents() as $item) {
echo "<span><a href='$item->url'>$item->title</a></span> ";
@@ -83,9 +86,9 @@
<!-- sidebar content -->
<?php if($sidebar): ?>
<div id='sidebar'>
<aside id='sidebar'>
<?php echo $sidebar; ?>
</div>
</aside>
<?php endif; ?>
</div>

View File

@@ -6,6 +6,7 @@
* 3. Main content and sidebar
* 4. Footer
* 5. Media queries for responsive layout
* 6. Accessibility helpers
*
*/
@@ -124,9 +125,6 @@ form.search {
border: 1px solid #ccc;
width: 100%;
}
form.search button {
display: none;
}
.breadcrumbs {
clear: both;
@@ -297,3 +295,42 @@ figure figcaption {
font-size: 115%;
}
}
/*********************************************************************
* 6. Accessibility helpers
*
*/
/* Hide visually, but remain approachable for screenreader */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
white-space: nowrap;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
border: 0;
}
/* Show bypass link on hover */
.element-focusable:focus {
clip: auto;
overflow: visible;
height: auto;
}
/* Sample styling for bypass link */
.bypass-to-main:focus {
top: 0;
left: 0;
width: 100%;
height: 40px;
line-height: 40px;
text-align: center;
background: #333;
color: #fff;
}

View File

@@ -26,6 +26,8 @@ if(!defined("PROCESSWIRE")) die();
/*** SITE CONFIG *************************************************************************/
/** @var Config $config */
/**
* Enable debug mode?
*

View File

@@ -144,10 +144,10 @@
"text": "Pfade sollten relativ zum Root-Verzeichnis ihrer ProcessWire Installation sein (d.h. wenn die Seite in einem Unterverzeichnis l\u00e4uft, diesen Teil weglassen)."
},
"9728a8f280cf7e8b0b89b52749f7e800": {
"text": "Bitte lesen Sie die [Anweisungen](https:\/\/github.com\/ryancramerdesign\/ProcessWire\/blob\/dev\/wire\/modules\/Inputfield\/InputfieldCKEditor\/README.md#custom-editor-css-file) zur Benutzung."
"text": "Bitte lesen Sie die [Anweisungen](https:\/\/github.com\/processwire\/processwire\/blob\/master\/wire\/modules\/Inputfield\/InputfieldCKEditor\/README.md#custom-editor-css-file) zur Benutzung."
},
"63ba0561c6027fed354e6c5af252a77b": {
"text": "Bitte lesen Sie die [Anweisungen](https:\/\/github.com\/ryancramerdesign\/ProcessWire\/blob\/dev\/wire\/modules\/Inputfield\/InputfieldCKEditor\/README.md#custom-editor-js-styles-set) zur Benutzung."
"text": "Bitte lesen Sie die [Anweisungen](https:\/\/github.com\/processwire\/processwire\/blob\/master\/wire\/modules\/Inputfield\/InputfieldCKEditor\/README.md#custom-editor-js-styles-set) zur Benutzung."
},
"8cd140c060d298234e0079cb77ee8190": {
"text": "Folgende Plugins wurden gefunden. Markieren Sie die Box neben jedem Plugin welches geladen werden soll."

View File

@@ -60,8 +60,10 @@
</head>
<body class="<?php if($sidebar) echo "has-sidebar"; ?>">
<a href="#main" class="visually-hidden element-focusable bypass-to-main"><?php echo _x('Skip to content', 'bypass'); ?></a>
<!-- language switcher / navigation -->
<ul class='languages'><?php
<ul class='languages' role='navigation'><?php
foreach($languages as $language) {
if(!$page->viewable($language)) continue; // is page viewable in this language?
if($language->id == $user->language->id) {
@@ -76,11 +78,11 @@
?></ul>
<!-- top navigation -->
<ul class='topnav'><?php
<ul class='topnav' role='navigation'><?php
// top navigation consists of homepage and its visible children
foreach($homepage->and($homepage->children) as $item) {
if($item->id == $page->rootParent->id) {
echo "<li class='current'>";
echo "<li class='current' aria-current='true'><span class='visually-hidden'>" . _x('Current page:', 'navigation') . " </span>";
} else {
echo "<li>";
}
@@ -92,7 +94,7 @@
?></ul>
<!-- breadcrumbs -->
<div class='breadcrumbs'><?php
<div class='breadcrumbs' role='navigation' aria-label='<?php echo _x('You are here:', 'breadcrumbs'); ?>'><?php
// breadcrumbs are the current page's parents
foreach($page->parents() as $item) {
echo "<span><a href='$item->url'>$item->title</a></span> ";
@@ -103,12 +105,13 @@
<!-- search engine -->
<form class='search' action='<?php echo $pages->get('template=search')->url; ?>' method='get'>
<input type='text' name='q' placeholder='<?php echo _x('Search', 'placeholder'); ?>' />
<button type='submit' name='submit'><?php echo _x('Search', 'button'); ?></button>
<label for='search' class='visually-hidden'><?php echo _x('Search:', 'label'); ?></label>
<input type='text' name='q' id='search' placeh older='<?php echo _x('Search', 'placeholder'); ?>' />
<button type='submit' name='submit' class='visually-hidden'><?php echo _x('Search', 'button'); ?></button>
</form>
<div id='main'>
<main id='main'>
<!-- main content -->
<div id='content'>
@@ -121,15 +124,15 @@
<!-- sidebar content -->
<?php if($sidebar): ?>
<div id='sidebar'>
<aside id='sidebar'>
<?php echo $sidebar; ?>
</div>
</aside>
<?php endif; ?>
</div>
</main>
<!-- footer -->
<footer id='footer'>

View File

@@ -6,6 +6,7 @@
* 3. Main content and sidebar
* 4. Footer
* 5. Media queries for responsive layout
* 6. Accessibility helpers
*
*/
@@ -156,9 +157,6 @@ form.search {
border: 1px solid #ccc;
width: 100%;
}
form.search button {
display: none;
}
.breadcrumbs {
font-size: 80%;
@@ -351,3 +349,43 @@ figure figcaption {
font-size: 115%;
}
}
/*********************************************************************
* 6. Accessibility helpers
*
*/
/* Hide visually, but remain approachable for screenreader */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
white-space: nowrap;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
border: 0;
}
/* Show bypass link on hover */
.element-focusable:focus {
clip: auto;
overflow: visible;
height: auto;
}
/* Sample styling for bypass link */
.bypass-to-main:focus {
top: 0;
left: 0;
width: 100%;
height: 40px;
line-height: 40px;
text-align: center;
background: #333;
color: #fff;
}

28
wire/.editorconfig Normal file
View File

@@ -0,0 +1,28 @@
; This file is for unifying the coding style for different editors and IDEs.
; More information at http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
[*.{php,inc,module}]
indent_style = tab
trim_trailing_whitespace = false
insert_final_newline = true
[*.js]
indent_style = tab
trim_trailing_whitespace = true
insert_final_newline = true
[*.{css,less,scss}]
indent_style = tab
trim_trailing_whitespace = true
insert_final_newline = true

View File

@@ -46,6 +46,9 @@ 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.
*
* #notes This enables debug mode for ALL requests. See the debugIf option for an alternative.
*
* @var bool
@@ -127,6 +130,18 @@ $config->demo = false;
*/
$config->useFunctionsAPI = false;
/**
* Enable use of front-end markup regions?
*
* When enabled, HTML elements with an "id" attribute that are output before the opening
* `<!doctype>` or `<html>` tag can replace elements in the document that have the same id.
* Also supports append, prepend, replace, remove, before and after options.
*
* @var bool
*
*/
$config->useMarkupRegions = false;
/*** 2. DATES & TIMES *************************************************************************/
@@ -253,6 +268,15 @@ $config->sessionChallenge = true;
* 12: Fingerprint the forwarded/client IP and useragent
* 14: Fingerprint the remote IP, forwarded/client IP and useragent (all).
*
* If using fingerprint in an environment where the users
* IP address may change during the session, you should
* fingerprint only the useragent, or disable fingerprinting.
*
* If using fingerprint with an AWS load balancer, you should
* use one of the options that uses the “client IP” rather than
* the “remote IP”, fingerprint only the useragent, or disable
* fingerprinting.
*
* @var int
*
*/
@@ -274,6 +298,17 @@ $config->sessionFingerprint = 1;
*/
$config->sessionCookieSecure = 1;
/**
* Cookie domain for sessions
*
* Enables a session to traverse multiple subdomains.
* Specify a string having “.domain.com” (with leading period) or NULL to disable (default/recommended).
*
* @var string|null
*
*/
$config->sessionCookieDomain = null;
/**
* Number of session history entries to record.
*
@@ -296,6 +331,24 @@ $config->sessionHistory = 0;
*/
$config->userAuthHashType = 'sha1';
/**
* Names (string) or IDs (int) of roles that are not allowed to login
*
* Note that you must create these roles yourself in the admin. When a user has
* one of these named roles, $session->login() will not accept a login from them.
* This affects the admin login form and any other login forms that use ProcessWires
* session system.
*
* The default value specifies a role name of "login-disabled", meaning if you create
* a role with that name, and assign it to a user, that user will no longer be able
* to login.
*
* @var array
*
*/
$config->loginDisabledRoles = array(
'login-disabled'
);
/*** 4. TEMPLATE FILES **************************************************************************/
@@ -509,6 +562,7 @@ $config->fileContentTypes = array(
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/x-png',
'svg' => 'image/svg+xml'
);
@@ -519,7 +573,8 @@ $config->fileContentTypes = array(
*
* #property bool upscaling Upscale if necessary to reach target size? (1=true, 0=false)
* #property bool cropping Crop if necessary to reach target size? (1=true, 0=false)
* #property bool autoRotation Automatically correct orientation?
* #property bool autoRotation Automatically correct orientation? (1=true, 0=false)
* #property bool interlace Use interlaced JPEGs by default? Recommended. (1=true, 0=false)
* #property string sharpening Sharpening mode, enter one of: none, soft, medium, strong
* #property int quality Image quality, enter a value between 1 and 100, where 100 is highest quality (and largest files)
* #property float defaultGamma Default gamma of 0.5 to 4.0 or -1 to disable gamma correction (default=2.0)
@@ -531,6 +586,7 @@ $config->imageSizerOptions = array(
'upscaling' => true, // upscale if necessary to reach target size?
'cropping' => true, // crop if necessary to reach target size?
'autoRotation' => true, // automatically correct orientation?
'interlace' => false, // use interlaced JPEGs by default? (recommended)
'sharpening' => 'soft', // sharpening: none | soft | medium | strong
'quality' => 90, // quality: 1-100 where higher is better but bigger
'hidpiQuality' => 60, // Same as above quality setting, but specific to hidpi images
@@ -590,8 +646,8 @@ $config->fileCompilerOptions = array(
'siteOnly' => false, // only allow compilation of files in /site/ directory
'showNotices' => true, // show notices about compiled files to superuser when logged in
'logNotices' => true, // log notices about compiled files and maintenance to file-compiler.txt log.
'chmodFile' => $config->chmodFile, // mode to use for created files, i.e. "0644"
'chmodDir' => $config->chmodDir, // mode to use for created directories, i.e. "0755"
'chmodFile' => '', // mode to use for created files, i.e. "0644"
'chmodDir' => '', // mode to use for created directories, i.e. "0755"
'exclusions' => array(), // exclude filenames or paths that start with any of these
'extensions' => array('php', 'module', 'inc'), // file extensions we compile
'cachePath' => $config->paths->cache . 'FileCompiler/', // path where compiled files are stored
@@ -663,6 +719,14 @@ $config->protectCSRF = true;
*/
$config->maxUrlSegments = 4;
/**
* Maximum length for any individual URL segment (default=128)
*
* @var int
*
*/
$config->maxUrlSegmentLength = 128;
/**
* Maximum URL/path slashes (depth) for request URLs
*
@@ -767,9 +831,7 @@ $config->dbCharset = 'utf8';
/**
* Database engine
*
* MyISAM is the recommended value, but you may also use InnoDB (experimental).
*
* Note that use of 'InnoDB' is currently experimental. Avoid changing this after install.
* May be 'InnoDB' or 'MyISAM'. Avoid changing this after install.
*
*/
$config->dbEngine = 'MyISAM';
@@ -864,8 +926,18 @@ $config->dbSqlModes = array(
"5.7.0" => "remove:STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BY"
);
/**
* A key=>value array of any additional driver-specific connection options.
*
* @var array
*
*/
$config->dbOptions = array();
/**
* Optional DB socket config for sites that need it (for most you should exclude this)
*
* @var string
*
*/
$config->dbSocket = '';
@@ -878,6 +950,17 @@ $config->dbSocket = '';
*/
$config->dbQueryLogMax = 500;
/**
* Remove 4-byte characters (like emoji) when dbEngine is not utf8mb4?
*
* When charset is not “utf8mb4” and this value is true, 4-byte UTF-8 characters are stripped
* out of inserted values when possible. Note that this can add some overhead to INSERTs.
*
* @var bool
*
*/
$config->dbStripMB4 = false;
/*** 8. MODULES *********************************************************************************/
@@ -928,6 +1011,28 @@ $config->substituteModules = array(
'InputfieldTinyMCE' => 'InputfieldCKEditor'
);
/**
* WireMail module(s) default settings
*
* Note you can add any other properties to the wireMail array that are supported by WireMail settings
* like weve done with from, fromName and headers here. Any values set here become defaults for the
* WireMail module.
*
* #property string module Name of WireMail module to use or blank to auto-detect. (default='')
* #property string from Default from email address, when none provided at runtime. (default=$config->adminEmail)
* #property string fromName Default from name string, when none provided at runtime. (default='')
* #property array headers Default additional headers to send in email, key=value. (default=[])
*
* @var array
*
*/
$config->wireMail = array(
'module' => '',
'from' => '',
'fromName' => '',
'headers' => array(),
);
/**
* PageList default settings
*
@@ -958,6 +1063,7 @@ $config->pageList = array(
* #property bool confirm Notify user if they attempt to navigate away from unsaved changes?
* #property bool ajaxChildren Whether to load the 'children' tab via ajax
* #property bool ajaxParent Whether to load the 'parent' field via ajax
* #property bool editCrumbs Whether or not breadcrumbs load page editor (false=load page list).
*
* @var array
*
@@ -967,8 +1073,19 @@ $config->pageEdit = array(
'confirm' => true,
'ajaxChildren' => true,
'ajaxParent' => true,
'editCrumbs' => false,
);
/**
* PageAdd default settings
*
* #property string noSuggestTemplates Disable suggestions for new pages (1=disable all, or specify template names separated by space)
*
*/
$config->pageAdd = array(
'noSuggestTemplates' => '',
);
/*** 9. MISC ************************************************************************************/
@@ -988,12 +1105,20 @@ $config->logs = array(
'exceptions',
);
/**
* Include IP address in logs, when applicable?
*
* @var bool
*
*/
$config->logIP = false;
/**
* Default admin theme
*
* Module name of default admin theme for guest and users that haven't already selected one
*
* Core options include: **AdminThemeDefault** or **AdminThemeReno**.
* Core options include: **AdminThemeDefault** or **AdminThemeReno** or **AdminThemeUikit**.
* Additional options will depend on what other 3rd party AdminTheme modules you have installed.
*
* @var string
@@ -1107,6 +1232,14 @@ $config->allowExceptions = false;
*/
$config->usePoweredBy = true;
/**
* Chunk size for lazy-loaded pages used by $pages->findMany()
*
* @var int
*
*/
$config->lazyPageChunkSize = 250;
/**
* Settings specific to InputfieldWrapper class
*
@@ -1123,6 +1256,7 @@ $config->usePoweredBy = true;
*
*/
/*** 10. RUNTIME ********************************************************************************
*
* The following are runtime-only settings and cannot be changed from /site/config.php
@@ -1141,6 +1275,12 @@ $config->https = null;
*/
$config->ajax = false;
/**
* modal: This is automatically set to TRUE when request is in a modal window.
*
*/
$config->modal = false;
/**
* external: This is automatically set to TRUE when PW is externally bootstrapped.
*

View File

@@ -15,6 +15,7 @@ namespace PHPSTORM_META {
\wire('') => [
'' == '@',
'config' instanceof \ProcessWire\Config,
'cache' instanceof \ProcessWire\WireCache,
'wire' instanceof \ProcessWire\ProcessWire,
'log' instanceof \ProcessWire\WireLog,
'notices' instanceof \ProcessWire\Notices,
@@ -46,6 +47,7 @@ namespace PHPSTORM_META {
// this one does not appear to work, leaving in case someone knows how to make it work
'' == '@',
'config' instanceof \ProcessWire\Config,
'cache' instanceof \ProcessWire\WireCache,
'wire' instanceof \ProcessWire\ProcessWire,
'log' instanceof \ProcessWire\WireLog,
'notices' instanceof \ProcessWire\Notices,

View File

@@ -17,6 +17,7 @@
*
* @method void install()
* @method void uninstall()
* @method array getExtraMarkup()
*
*/
@@ -61,6 +62,22 @@ abstract class AdminTheme extends WireData implements Module {
*/
protected $bodyClasses = array();
/**
* Extra markup regions
*
* @var array
*
*/
protected $extraMarkup = array(
'head' => '',
'notices' => '',
'body' => '',
'masthead' => '',
'content' => '',
'footer' => '',
'sidebar' => '', // sidebar not used in all admin themes
);
/**
* URLs to place in link prerender tags
*
@@ -78,9 +95,11 @@ abstract class AdminTheme extends WireData implements Module {
}
/**
* Initialize the admin theme systme and determine which admin theme should be used
* Initialize the admin theme system and determine which admin theme should be used
*
* All admin themes must call this init() method to register themselves.
*
* Note: this should be called after API ready.
*
*/
public function init() {
@@ -99,40 +118,38 @@ abstract class AdminTheme extends WireData implements Module {
// if admin theme has already been set, then no need to continue
if($this->wire('adminTheme')) return;
$isCurrent = false;
/** @var Config $config */
$config = $this->wire('config');
/** @var Session $session */
$session = $this->wire('session');
/** @var string $adminTheme */
$adminTheme = $this->wire('user')->admin_theme;
if($adminTheme) {
// there is user specified admin theme
// check if this is the one that should be used
if($adminTheme == $this->className()) $isCurrent = true;
if($adminTheme == $this->className()) $this->setCurrent();
} else if($this->wire('config')->defaultAdminTheme == $this->className()) {
// there is no user specified admin theme, so use this one
$isCurrent = true;
}
// set as an API variable and populate configuration variables
if($isCurrent) {
$this->wire('adminTheme', $this);
$this->config->paths->set('adminTemplates', $this->config->paths->get($this->className()));
$this->config->urls->set('adminTemplates', $this->config->urls->get($this->className()));
$this->setCurrent();
}
// adjust $config adminThumbOptions[scale] for auto detect when requested
$o = $this->wire('config')->adminThumbOptions;
$o = $config->adminThumbOptions;
if($o && isset($o['scale']) && $o['scale'] === 1) {
$o['scale'] = $this->wire('session')->hidpi ? 0.5 : 1.0;
$this->wire('config')->adminThumbOptions = $o;
$o['scale'] = $session->hidpi ? 0.5 : 1.0;
$config->adminThumbOptions = $o;
}
$this->config->js('modals', $this->config->modals);
$config->js('modals', $config->modals);
if($session->hidpi) $this->addBodyClass('hidpi-device');
if($session->touch) $this->addBodyClass('touch-device');
if($this->wire('session')->hidpi) $this->addBodyClass('hidpi-device');
if($this->wire('session')->touch) $this->addBodyClass('touch-device');
$this->addBodyClass($this->className());
}
public function get($key) {
if($key == 'version') return $this->version;
return parent::get($key);
@@ -146,6 +163,19 @@ abstract class AdminTheme extends WireData implements Module {
return $this->wire('adminTheme') === $this;
}
/**
* Set this admin theme as the current one
*
*/
protected function setCurrent() {
$config = $this->wire('config');
$name = $this->className();
$config->paths->set('adminTemplates', $config->paths->get($name));
$config->urls->set('adminTemplates', $config->urls->get($name));
$config->set('defaultAdminTheme', $name);
$this->wire('adminTheme', $this);
}
/**
* Enables hooks to append extra markup to various sections of the admin page
*
@@ -160,15 +190,7 @@ abstract class AdminTheme extends WireData implements Module {
*
*/
public function ___getExtraMarkup() {
$parts = array(
'head' => '',
'notices' => '',
'body' => '',
'masthead' => '',
'content' => '',
'footer' => '',
'sidebar' => '', // sidebar not used in all admin themes
);
$parts = $this->extraMarkup;
$isLoggedin = $this->wire('user')->isLoggedin();
if($isLoggedin && $this->wire('modules')->isInstalled('InputfieldCKEditor')
&& $this->wire('process') instanceof WirePageEditor) {
@@ -177,19 +199,48 @@ abstract class AdminTheme extends WireData implements Module {
"window.CKEDITOR_BASEPATH='" . $this->wire('config')->urls->InputfieldCKEditor .
'ckeditor-' . InputfieldCKEditor::CKEDITOR_VERSION . "/';</script>";
}
/*
if($isLoggedin && $this->wire('config')->advanced) {
$parts['footer'] = "<p class='AdvancedMode'><i class='fa fa-flask'></i> " . $this->_('Advanced Mode') . "</p>";
}
*/
foreach($this->preRenderURLs as $url) {
$parts['head'] .= "<link rel='prerender' href='$url'>";
}
return $parts;
}
/**
* Add extra markup to a region in the admin theme
*
* @param string $name
* @param string $value
*
*/
public function addExtraMarkup($name, $value) {
if(!empty($this->extraMarkup[$name])) {
$this->extraMarkup[$name] .= "\n$value";
} else {
$this->extraMarkup[$name] = $value;
}
}
/**
* Add a <body> class to the admin theme
*
* @param string $className
*
*/
public function addBodyClass($className) {
$this->bodyClasses[$className] = $className;
}
/**
* Get the body[class] attribute string
*
* @return string
*
*/
public function getBodyClass() {
return trim(implode(' ', $this->bodyClasses));
}
@@ -213,36 +264,44 @@ abstract class AdminTheme extends WireData implements Module {
// we already have this field installed, no need to continue
if($field) {
$this->message($toUseNote);
return;
} else {
// this will be the 2nd admin theme installed, so add a field that lets them select admin theme
$field = $this->wire(new Field());
$field->name = 'admin_theme';
$field->type = $this->wire('modules')->get('FieldtypeModule');
$field->set('moduleTypes', array('AdminTheme'));
$field->set('labelField', 'title');
$field->set('inputfieldClass', 'InputfieldRadios');
$field->label = 'Admin Theme';
$field->flags = Field::flagSystem;
try {
$field->save();
} catch(\Exception $e) {
// $this->error("Error creating 'admin_theme' field: " . $e->getMessage());
}
}
// this will be the 2nd admin theme installed, so add a field that lets them select admin theme
$field = $this->wire(new Field());
$field->name = 'admin_theme';
$field->type = $this->wire('modules')->get('FieldtypeModule');
$field->set('moduleTypes', array('AdminTheme'));
$field->set('labelField', 'title');
$field->set('inputfieldClass', 'InputfieldRadios');
$field->label = 'Admin Theme';
$field->flags = Field::flagSystem;
$field->save();
$fieldgroup = $this->wire('fieldgroups')->get('user');
$fieldgroup->add($field);
$fieldgroup->save();
// make this field one that the user is allowed to configure in their profile
$data = $this->wire('modules')->getModuleConfigData('ProcessProfile');
$data['profileFields'][] = 'admin_theme';
$this->wire('modules')->saveModuleConfigData('ProcessProfile', $data);
$this->message($this->_('Installed field "admin_theme" and added to user profile settings.'));
$this->message($toUseNote);
if($field && $field->id) {
/** @var Fieldgroup $fieldgroup */
$fieldgroup = $this->wire('fieldgroups')->get('user');
if(!$fieldgroup->hasField($field)) {
$fieldgroup->add($field);
$fieldgroup->save();
$this->message($this->_('Installed field "admin_theme" and added to user profile settings.'));
$this->message($toUseNote);
}
// make this field one that the user is allowed to configure in their profile
$data = $this->wire('modules')->getModuleConfigData('ProcessProfile');
$data['profileFields'][] = 'admin_theme';
$this->wire('modules')->saveModuleConfigData('ProcessProfile', $data);
}
}
/**
* Set a pre-render URL or get currently pre-render URL(s)
*
* #pw-internal
*
* @param string $url
* @return array
*
@@ -253,6 +312,14 @@ abstract class AdminTheme extends WireData implements Module {
}
public function ___uninstall() {
$defaultAdminTheme = $this->wire('config')->defaultAdminTheme;
if($defaultAdminTheme == $this->className()) {
throw new WireException(
"Cannot uninstall this admin theme because \$config->defaultAdminTheme = '$defaultAdminTheme'; " .
"Please add this setting with a different value in /site/config.php"
);
}
/*
if(self::$numAdminThemes > 1) return;

View File

@@ -0,0 +1,809 @@
<?php namespace ProcessWire;
/**
* AdminTheme Framework
*
* The methods in this class may eventually be merged to AdminTheme.php,
* but are isolated to this class during development.
*
* @property bool $isSuperuser
* @property bool $isEditor
* @property bool $isLoggedIn
* @property bool $isModal
* @property bool|int $useAsLogin
* @method array getUserNavArray()
*
*/
abstract class AdminThemeFramework extends AdminTheme {
/**
* Is there currently a logged in user?
*
* @var bool
*
*/
protected $isLoggedIn = false;
/**
* Is user logged in with page-edit permission?
*
* @var bool
*
*/
protected $isEditor = false;
/**
* Is current user a superuser?
*
* @var bool
*
*/
protected $isSuperuser = false;
/**
* Is the current request a modal request?
*
* @var bool|string Either false, true, or "inline"
*
*/
protected $isModal = false;
/**
* @var Sanitizer
*
*/
protected $sanitizer;
/**
* Construct
*
*/
public function __construct() {
parent::__construct();
$this->set('useAsLogin', false);
$this->sanitizer = $this->wire('sanitizer');
}
/**
* Override get() method from WireData to support additional properties
*
* @param string $key
* @return bool|int|mixed|null|string
*
*/
public function get($key) {
switch($key) {
case 'isSuperuser': $value = $this->isSuperuser; break;
case 'isEditor': $value = $this->isEditor; break;
case 'isLoggedIn': $value = $this->isLoggedIn; break;
case 'isModal': $value = $this->isModal; break;
default: $value = parent::get($key);
}
return $value;
}
/**
* Initialize and attach hooks
*
* Note: descending classes should call this after API ready
*
*/
public function init() {
$user = $this->wire('user');
if(!$user->isLoggedin() && $this->useAsLogin) $this->setCurrent();
parent::init();
// if this is not the current admin theme, exit now so no hooks are attached
if(!$this->isCurrent()) return;
$this->isLoggedIn = $user->isLoggedin();
$this->isSuperuser = $this->isLoggedIn && $user->isSuperuser();
$this->isEditor = $this->isLoggedIn && ($this->isSuperuser || $user->hasPermission('page-edit'));
$this->includeInitFile();
$modal = $this->wire('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();
}
/**
* Include the admin theme init file
*
*/
public function includeInitFile() {
$config = $this->wire('config');
$initFile = $config->paths->adminTemplates . '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);
}
/** @noinspection PhpIncludeInspection */
include_once($initFile);
}
}
/**
* Perform a translation, based on text from shared admin file: /wire/templates-admin/default.php
*
* @param string $text
* @return string
*
*/
public function _($text) {
static $translate = null;
if(is_null($translate)) $translate = $this->wire('languages') !== null;
if($translate === false) return $text;
$value = __($text, $this->wire('config')->paths->root . 'wire/templates-admin/default.php');
if($value === $text) $value = parent::_($text);
return $value;
}
/**
* Get the current page headline
*
* @return string
*
*/
public function getHeadline() {
$headline = $this->wire('processHeadline');
if(!$headline) $headline = $this->wire('page')->get('title|name');
if($this->wire('languages')) $headline = $this->_($headline);
return $this->sanitizer->entities1($headline);
}
/**
* Get navigation title for the given page, return blank if page should not be shown
*
* @param Page $p
* @return string
*
*/
public function getPageTitle(Page $p) {
if($p->name == 'add' && $p->parent->name == 'page') {
$title = $this->getAddNewLabel();
} else {
$title = $this->_($p->title);
}
$title = $this->sanitizer->entities1($title);
return $title;
}
/**
* Get icon used by the given page
*
* @param Page $p
* @return mixed|null|string
*
*/
public function getPageIcon(Page $p) {
$icon = '';
if($p->template == 'admin') {
$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
$pageIcon = $p->get('page_icon');
if($pageIcon) $icon = $pageIcon;
if(!$icon) switch($p->id) {
case 22: $icon = 'gears'; break; // Setup
case 21: $icon = 'plug'; break; // Modules
case 28: $icon = 'key'; break; // Access
}
if(!$icon && $p->parent->id != $this->wire('config')->adminRootPageID) {
$icon = 'file-o ui-priority-secondary';
}
return $icon;
}
/**
* Get “Add New” button actions
*
* - Returns array of arrays, each with 'url', 'label' and 'icon' properties.
* - Returns empty array if Add New button should not be displayed.
*
* @return array
*
*/
public function getAddNewActions() {
$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($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));
$data = $module->executeNavJSON(array('getArray' => true));
$actions = array();
foreach($data['list'] as $item) {
$item['url'] = $data['url'] . $item['url'];
$actions[] = $item;
}
return $actions;
}
/**
* Get the translated “Add New” label thats used in a couple spots
*
* @return string
*
*/
public function getAddNewLabel() {
return $this->_('Add New');
}
/**
* Get the classes that will be used in the <body class=''> tag
*
* @return string
*
*/
public function getBodyClass() {
$page = $this->wire('page');
$process = $this->wire('process');
$classes = array(
"id-{$page->id}",
"template-{$page->template->name}",
"pw-init",
parent::getBodyClass(),
);
if($this->isModal) $classes[] = 'modal';
if($this->isModal === 'inline') $classes[] = 'modal-inline';
if($this->wire('input')->urlSegment1) $classes[] = 'hasUrlSegments';
if($process) $classes[] = $process->className();
return implode(' ', $classes);
}
/**
* Get Javascript that must be present in the document <head>
*
* @return string
*
*/
public function getHeadJS() {
/** @var Config $config */
$config = $this->wire('config');
/** @var Paths $urls */
$urls = $config->urls;
/** @var array $jsConfig */
$jsConfig = $config->js();
$jsConfig['debug'] = $config->debug;
$jsConfig['urls'] = array(
'root' => $urls->root,
'admin' => $urls->admin,
'modules' => $urls->modules,
'core' => $urls->core,
'files' => $urls->files,
'templates' => $urls->templates,
'adminTemplates' => $urls->adminTemplates,
);
$out =
"var ProcessWire = { config: " . wireEncodeJSON($jsConfig, true, $config->debug) . " }; " .
"var config = ProcessWire.config;\n"; // legacy support
return $out;
}
/**
* Allow the given Page to appear in admin theme navigation?
*
* @param Page $p Page to test
* @param PageArray|array $children Children of page, if applicable (optional)
* @param string|null $permission Specify required permission (optional)
* @return bool
*
*/
public function allowPageInNav(Page $p, $children = array(), $permission = null) {
if($this->isSuperuser) return true;
$pageViewable = $p->viewable();
if(!$pageViewable) return false;
$allow = false;
$numChildren = count($children);
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');
if($numAddable === null) {
/** @var ProcessPageAdd $processPageAdd */
$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);
}
}
// no addable options, so do not show the "Add New" item
if(!$numAddable) return false;
} else if(empty($permission)) {
// no permission specified
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;
} 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);
if(!empty($moduleInfo['permission'])) $permission = $moduleInfo['permission'];
}
}
if($permission) {
// specific permission required to determine view access
$allow = $this->wire('user')->hasPermission($permission);
} else if($pageViewable && $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)) {
$allow = true;
break;
}
}
}
return $allow;
}
/**
* Return nav array of primary navigation
*
* @return array
*
*/
public function getPrimaryNavArray() {
$items = array();
$config = $this->wire('config');
$admin = $this->wire('pages')->get($config->adminRootPageID);
foreach($admin->children("check_access=0") as $p) {
$item = $this->pageToNavArray($p);
if($item) $items[] = $item;
}
return $items;
}
/**
* Get navigation array from a Process module
*
* @param array|Module|string $module Module info array or Module object or string
* @param Page $p Page upon which the Process module is contained
* @return array
*
*/
public function moduleToNavArray($module, Page $p) {
$config = $this->wire('config');
$modules = $this->wire('modules');
$textdomain = str_replace($config->paths->root, '/', $modules->getModuleFile($p->process));
$user = $this->wire('user');
$navArray = array();
if(is_array($module)) {
$moduleInfo = $module;
} else {
$moduleInfo = $modules->getModuleInfo($module);
}
foreach($moduleInfo['nav'] as $navItem) {
$permission = empty($navItem['permission']) ? '' : $navItem['permission'];
if($permission && !$user->hasPermission($permission)) continue;
$navArray[] = array(
'id' => 0,
'parent_id' => $p->id,
'title' => $this->sanitizer->entities1(__($navItem['label'], $textdomain)), // translate from context of Process module
'name' => '',
'url' => $p->url . $navItem['url'],
'icon' => empty($navItem['icon']) ? '' : $navItem['icon'],
'children' => array(),
'navJSON' => empty($navItem['navJSON']) ? '' : $p->url . $navItem['navJSON'],
);
}
return $navArray;
}
/**
* Get a navigation array the given Page, or null if page not allowed in nav
*
* @param Page $p
* @return array|null
*
*/
public function pageToNavArray(Page $p) {
$children = $p->numChildren ? $p->children("check_access=0") : array();
if(!$this->allowPageInNav($p, $children)) return null;
$navArray = array(
'id' => $p->id,
'parent_id' => $p->parent_id,
'url' => $p->url,
'name' => $p->name,
'title' => $this->getPageTitle($p),
'icon' => $this->getPageIcon($p),
'children' => array(),
'navJSON' => '',
);
if(!count($children)) {
// no children available
if($p->template == 'admin' && $p->process) {
// see if process module defines its own navigation
$moduleInfo = $this->wire('modules')->getModuleInfo($p->process);
if(!empty($moduleInfo['nav'])) {
$navArray['children'] = $this->moduleToNavArray($moduleInfo, $p);
}
} else {
// The /page/ and /page/list/ are the same process, so just keep them on /page/ instead.
if(strpos($navArray['url'], '/page/list/') !== false) {
$navArray['url'] = str_replace('/page/list/', '/page/', $navArray['url']);
}
}
return $navArray;
}
// if we reach this point, then we have a PageArray of children
$modules = $this->wire('modules');
foreach($children as $c) {
if(!$c->process) continue;
$moduleInfo = $modules->getModuleInfo($c->process);
$permission = empty($moduleInfo['permission']) ? '' : $moduleInfo['permission'];
if(!$this->allowPageInNav($c, array(), $permission)) continue;
$childItem = array(
'id' => $c->id,
'parent_id' => $c->parent_id,
'title' => $this->getPageTitle($c),
'name' => $c->name,
'url' => $c->url,
'icon' => $this->getPageIcon($c),
'children' => array(),
'navJSON' => empty($moduleInfo['useNavJSON']) ? '' : $c->url . 'navJSON/',
);
if(!empty($moduleInfo['nav'])) {
$childItem['children'] = $this->moduleToNavArray($moduleInfo, $c);
}
$navArray['children'][] = $childItem;
} // foreach
return $navArray;
}
/**
* Get navigation items for the “user” menu
*
* This is hookable so that something else could add stuff to it.
* See the method body for details on format used.
*
* @return array
*
*/
public function ___getUserNavArray() {
$urls = $this->wire('urls');
$navArray = array();
$navArray[] = array(
'url' => $urls->root,
'title' => $this->_('View site'),
'target' => '_top',
'icon' => 'eye',
);
if($this->wire('user')->hasPermission('profile-edit')) $navArray[] = array(
'url' => $urls->admin . 'profile/',
'title' => $this->_('Profile'),
'icon' => 'user',
'permission' => 'profile-edit',
);
$navArray[] = array(
'url' => $urls->admin . 'login/logout/',
'title' => $this->_('Logout'),
'target' => '_top',
'icon' => 'power-off',
);
return $navArray;
}
/**
* Get the browser <title>
*
* @return string
*
*/
public function getBrowserTitle() {
$browserTitle = $this->wire('processBrowserTitle');
$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';
}
if(!$modal) {
$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";
}
return $this->sanitizer->entities1($browserTitle);
}
/**
* Test all notice types
*
* @return bool
*
*/
public function testNotices() {
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);
$this->warning('Warning test');
$this->warning('Warning test debug', Notice::debug);
$this->warning('Warning test markup <a href="#">example</a>', Notice::allowMarkup);
$this->error('Error test');
$this->error('Error test debug', Notice::debug);
$this->error('Error test markup <a href="#">example</a>', Notice::allowMarkup);
return true;
}
/**
* Render runtime notices div#notices
*
* @param Notices|bool $notices Notices object or specify boolean true to return array of all available $options
* @param array $options See defaults in method
* @return string|array Returns string unless you specify true for $notices argument, then it returns an array.
*
*/
public function renderNotices($notices, array $options = array()) {
$defaults = array(
'messageClass' => 'NoticeMessage', // class for messages
'messageIcon' => 'check-square', // default icon to show with notices
'warningClass' => 'NoticeWarning', // class for warnings
'warningIcon' => 'exclamation-circle', // icon for warnings
'errorClass' => 'NoticeError', // class for errors
'errorIcon' => 'exclamation-triangle', // icon for errors
'debugClass' => 'NoticeDebug', // class for debug items (appended)
'debugIcon' => 'bug', // icon for debug notices
'closeClass' => 'pw-notice-remove notice-remove', // class for close notices link <a>
'closeIcon' => 'times', // icon for close notices link
'listMarkup' => "<ul class='pw-notices' id='notices'>{out}</ul><!--/notices-->",
'itemMarkup' => "<li class='{class}'>{remove}{icon}{text}</li>",
// the following apply only when groupByType==true
'groupByType' => true, // Group notices by type
'groupParentClass' => 'pw-notice-group-parent', // class for parent notices
'groupChildClass' => 'pw-notice-group-child', // class for children (of parent notices)
'groupToggleMarkup' => "<a class='pw-notice-group-toggle' href='#'>{label}" .
"<i class='fa fa-fw fa-bell-o' data-toggle='fa-bell-o fa-bell'></i>" .
"<i class='fa fa-fw fa-angle-right' data-toggle='fa-angle-right fa-angle-down'></i></a>",
'groupToggleLabel' => $this->_("+{n-1}"),
);
$options = array_merge($defaults, $options);
if($notices === true) return $options;
$config = $this->wire('config');
$noticesArray = array();
$out = '';
$removeIcon = $this->renderIcon($options['closeIcon']);
$removeLabel = $this->_('Close all');
$removeLink = "<a class='$options[closeClass]' href='#' title='$removeLabel'>$removeIcon</a>";
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) {
$text = $notice->text;
$allowMarkup = $notice->flags & Notice::allowMarkup;
if($allowMarkup) {
// leave $text alone
} else {
// unencode + re-encode entities, just in case module already entity some or all of output
if(strpos($text, '&') !== false) $text = $this->sanitizer->unentities($text);
$text = $this->sanitizer->entities($text);
}
if($notice instanceof NoticeError) {
$class = $options['errorClass'];
$icon = $options['errorIcon'];
$noticeType = 'errors';
} else if($notice instanceof NoticeWarning) {
$class = $options['warningClass'];
$icon = $options['warningIcon'];
$noticeType = 'warnings';
} else {
$class = $options['messageClass'];
$icon = $options['messageIcon'];
$noticeType = 'messages';
}
if($notice->flags & Notice::debug) {
$class .= " " . $options['debugClass'];
$icon = $options['debugIcon'];
// ensure non-debug version is set as well
if(!isset($noticesArray[$noticeType])) $noticesArray[$noticeType] = array();
$noticeType .= "-debug";
}
// indicate which class the notice originated from in debug mode
if($notice->class && $config->debug) $text = "{$notice->class}: $text";
$replacements = array(
'{class}' => $class,
'{remove}' => '',
'{icon}' => $this->renderNavIcon($notice->icon ? $notice->icon : $icon),
'{text}' => $text,
);
if($options['groupByType']) {
if(!isset($noticesArray[$noticeType])) $noticesArray[$noticeType] = array();
$noticesArray[$noticeType][] = $replacements;
} else {
if($n === 0) $replacements['{remove}'] = $removeLink;
$out .= str_replace(array_keys($replacements), array_values($replacements), $options['itemMarkup']);
}
}
if($options['groupByType']) {
$cnt = 0;
foreach($noticesArray as $noticeType => $noticeReplacements) {
if(strpos($noticeType, '-debug')) continue;
if(isset($noticesArray["$noticeType-debug"])) {
$noticeReplacements = array_merge($noticeReplacements, $noticesArray["$noticeType-debug"]);
}
$n = count($noticeReplacements);
if($n > 1) {
$notice =& $noticeReplacements[0];
$label = str_replace(array('{n}', '{n-1}'), array($n, $n-1), $options['groupToggleLabel']);
$notice['{text}'] .= ' ' . str_replace(array('{label}'), array($label), $options['groupToggleMarkup']);
$notice['{class}'] .= ' ' . $options['groupParentClass'];
$childClass = $options['groupChildClass'];
} else {
$childClass = '';
}
foreach($noticeReplacements as $i => $replacements) {
if(!$cnt) $replacements['{remove}'] = $removeLink;
if($childClass && $i > 0) $replacements['{class}'] .= ' ' . $childClass;
$out .= str_replace(array_keys($replacements), array_values($replacements), $options['itemMarkup']);
$cnt++;
}
}
}
$out = str_replace('{out}', $out, $options['listMarkup']);
$out .= $this->renderExtraMarkup('notices');
return $out;
}
/**
* Render markup for a font-awesome icon
*
* @param string $icon Name of icon to render, excluding the “fa-” prefix
* @param bool $fw Specify true to make fixed width (default=false).
* @return string
*
*/
public function renderIcon($icon, $fw = false) {
if($fw) $icon .= ' fa-fw';
return "<i class='fa fa-$icon'></i>";
}
/**
* Render markup for a font-awesome icon that precedes a navigation label
*
* This is the same as renderIcon() except that fixed-width is assumed and a "nav-nav-icon"
* class is added to it.
*
* @param string $icon Name of icon to render, excluding the “fa-” prefix
* @return string
*
*/
public function renderNavIcon($icon) {
return $this->renderIcon("$icon pw-nav-icon", true);
}
/**
* Render an extra markup region
*
* @param string $for
* @return mixed|string
*
*/
public function renderExtraMarkup($for) {
static $extras = array();
if(empty($extras)) $extras = $this->getExtraMarkup();
return isset($extras[$for]) ? $extras[$for] : '';
}
/**
* Module Configuration
*
* @param InputfieldWrapper $inputfields
*
*/
public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {
/** @var InputfieldCheckbox $f */
$f = $this->modules->get('InputfieldCheckbox');
$f->name = 'useAsLogin';
$f->label = $this->_('Use this admin theme for login screen?');
$f->description = $this->_('When checked, this admin theme will be used on the user login screen.');
$f->icon = 'sign-in';
$f->collapsed = Inputfield::collapsedBlank;
if($this->get('useAsLogin')) $f->attr('checked', 'checked');
$inputfields->add($f);
if($f->attr('checked') && $this->input->requestMethod('GET')) {
$class = $this->className();
foreach($this->modules->findByPrefix('AdminTheme') as $name) {
if($name == $class) continue;
$cfg = $this->modules->getConfig($name);
if(!empty($cfg['useAsLogin'])) {
unset($cfg['useAsLogin']);
$this->modules->saveConfig($name, $cfg);
$this->message("Removed 'useAsLogin' setting from $name", Notice::debug);
}
}
}
}
}

View File

@@ -12,14 +12,20 @@
* https://processwire.com
*
* #pw-summary Holds ProcessWire configuration settings as defined in /wire/config.php and /site/config.php.
* #pw-body =
* For more detailed descriptions of these $config properties, including default values, see the
* [/wire/config.php](https://github.com/processwire/processwire/blob/master/wire/config.php) file.
* #pw-body
*
*
* @see /wire/config.php for more detailed descriptions of all config properties.
*
* @property bool $ajax If the current request is an ajax (asynchronous javascript) request, this is set to true. #pw-group-runtime
* @property bool|int $modal If the current request is in a modal window, this is set to a positive number. False if not. #pw-group-runtime
* @property string $httpHost Current HTTP host name. #pw-group-HTTP-and-input
* @property bool $https If the current request is an HTTPS request, this is set to true. #pw-group-runtime
* @property string $version Current ProcessWire version string (i.e. "2.2.3") #pw-group-system #pw-group-runtime
* @property int $systemVersion System version, used by SystemUpdater to determine when updates must be applied. #pw-group-system #pw-group-runtime
*
* @property FilenameArray $styles Array used by ProcessWire admin to keep track of what stylesheet files its template should load. It will be blank otherwise. Feel free to use it for the same purpose in your own sites. #pw-group-runtime
* @property FilenameArray $scripts Array used by ProcessWire admin to keep track of what javascript files its template should load. It will be blank otherwise. Feel free to use it for the same purpose in your own sites. #pw-group-runtime
@@ -33,7 +39,7 @@
*
* @property bool $protectCSRF Enables CSRF (cross site request forgery) protection on all PW forms, recommended for security. #pw-group-HTTP-and-input
*
* @property array $imageSizerOptions Default value is array('upscaling' => true, 'cropping' => true, 'quality' => 90) #pw-group-images
* @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 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 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
@@ -51,17 +57,19 @@
* @property string $sessionName Default session name to use (default='wire') #pw-group-session
* @property string $sessionNameSecure Session name when on HTTPS. Used when the sessionCookieSecure option is enabled (default). When blank (default), it will assume sessionName + 's'. #pw-group-session
* @property bool|int $sessionCookieSecure Use secure cookies when on HTTPS? When enabled, separate sessions will be maintained for HTTP vs. HTTPS. Good for security but tradeoff is login session may be lost when switching (default=1 or true). #pw-group-session
* @property null|string $sessionCookieDomain Domain to use for sessions, which enables a session to work across subdomains, or NULL to disable (default/recommended). #pw-group-session
* @property bool|callable $sessionAllow Are sessions allowed? Typically boolean true, unless provided a callable function that returns boolean. See /wire/config.php for an example. #pw-group-session
* @property int $sessionExpireSeconds How many seconds of inactivity before session expires? #pw-group-session
* @property bool $sessionChallenge Should login sessions have a challenge key? (for extra security, recommended) #pw-group-session
* @property bool $sessionFingerprint Should login sessions be tied to IP and user agent? May conflict with dynamic IPs. #pw-group-session
* @property bool $sessionFingerprint Should login sessions be tied to IP and user agent? 0 or false: Fingerprint off. 1 or true: Fingerprint on with default/recommended setting (currently 10). 2: Fingerprint only the remote IP. 4: Fingerprint only the forwarded/client IP (can be spoofed). 8: Fingerprint only the useragent. 10: Fingerprint the remote IP and useragent (default). 12: Fingerprint the forwarded/client IP and useragent. 14: Fingerprint the remote IP, forwarded/client IP and useragent (all). #pw-group-session
* @property int $sessionHistory Number of session entries to keep (default=0, which means off). #pw-group-session
* @property array $loginDisabledRoles Array of role name(s) or ID(s) of roles where login is disallowed. #pw-group-session
*
* @property string $prependTemplateFile PHP file in /site/templates/ that will be loaded before each page's template file (default=none) #pw-group-template-files
* @property string $appendTemplateFile PHP file in /site/templates/ that will be loaded after each page's template file (default=none) #pw-group-template-files
* @property bool $templateCompile Allow use of compiled templates? #pw-group-template-files
*
* @property string $uploadUnzipCommand Shell command to unzip archives, used by WireUpload class. @deprecated #pw-group-deprecated
* @property string $uploadUnzipCommand Shell command to unzip archives, used by WireUpload class. #pw-group-deprecated
* @property string $uploadTmpDir Optionally override PHP's upload_tmp_dir with your own. Should include a trailing slash. #pw-group-files
* @property string $uploadBadExtensions Space separated list of file extensions that are always disallowed from uploads. #pw-group-files
*
@@ -72,12 +80,13 @@
* @property string $pageNumUrlPrefix Prefix used for pagination URLs. Default is "page", resulting in "/page1", "/page2", etc. #pw-group-URLs
* @property array $pageNumUrlPrefixes Multiple prefixes that may be used for detecting pagination (internal use, for multi-language) #pw-group-URLs
* @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 string $wireInputOrder Order that variables with the $input API var are handled when you access $input->var. #pw-group-HTTP-and-input
*
* @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 $debug Special mode for use when debugging or developing a site. Recommended TRUE when site is in development and FALSE when not. #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 array $debugTools Tools, and their order, to show in debug mode (admin) #pw-group-system
*
@@ -92,23 +101,27 @@
* @property string $dbUser Database user #pw-group-database
* @property string $dbPass Database password #pw-group-database
* @property string $dbPort Database port (default=3306) #pw-group-database
* @property string $dbCharset Default is 'utf8' #pw-group-database
* @property string $dbCharset Default is 'utf8' but 'utf8mb4' is also supported. #pw-group-database
* @property string $dbEngine Database engine (MyISAM or InnoDB) #pw-group-database
* @property string $dbSocket Optional DB socket config for sites that need it. #pw-group-database
* @property bool $dbCache Whether to allow MySQL query caching. #pw-group-database
* @property bool $dbLowercaseTables Force any created field_* tables to be lowercase. #pw-group-database
* @property string $dbEngine Database engine (MyISAM or InnoDB) #pw-group-database
* @property string $dbPath MySQL database exec path (Path to mysqldump) #pw-group-database
* @property array $dbOptions Any additional driver options to pass as $options argument to "new PDO(...)". #pw-group-database
* @property array $dbSqlModes Set or adjust SQL mode per MySQL version, where array keys are MySQL version and values are SQL mode command(s). #pw-group-database
* @property int $dbQueryLogMax Maximum number of queries WireDatabasePDO will log in memory, when debug mode is enabled (default=1000). #pw-group-database
* @property string $dbInitCommand Database init command, for PDO::MYSQL_ATTR_INIT_COMMAND. Note placeholder {charset} gets replaced with $config->dbCharset. #pw-group-database
* $property array $dbSqlModes Set, add or remove SQL mode based on MySQL version. See default in /wire/config.php for details. #pw-group-database
* @property bool $dbStripMB4 When dbEngine is not utf8mb4 and this is true, we will attempt to remove 4-byte characters (like emoji) from inserts when possible. Note that this adds some overhead. #pw-group-database
*
* @property array $pageList Settings specific to Page lists. #pw-group-modules
* @property array $pageEdit Settings specific to Page editors. #pw-group-modules
* @property array $pageAdd Settings specific to Page adding. #pw-group-modules
* @property string $moduleServiceURL URL where the modules web service can be accessed #pw-group-modules
* @property string $moduleServiceKey API key for modules web service #pw-group-modules
* @property bool $moduleCompile Allow use of compiled modules? #pw-group-modules
* @property array $wireMail Default WireMail module settings. #pw-group-modules
*
* @property array $substituteModules Associative array with names of substitutute modules for when requested module doesn't exist #pw-group-modules
* @property array $substituteModules Associative array with names of substitute modules for when requested module doesn't exist #pw-group-modules
* @property array $logs Additional core logs to keep #pw-group-admin
* @property string $defaultAdminTheme Default admin theme: AdminThemeDefault or AdminThemeReno #pw-group-admin
* @property string $fatalErrorHTML HTML used for fatal error messages in HTTP mode. #pw-group-system
@@ -117,6 +130,8 @@
* @property bool $allowExceptions Allow Exceptions to propagate? (default=false, specify true only if you implement your own exception handler) #pw-group-system
* @property bool $usePoweredBy Use the x-powered-by header? Set to false to disable. #pw-group-system
* @property bool $useFunctionsAPI Allow most API variables to be accessed as functions? (see /wire/core/FunctionsAPI.php) #pw-group-system
* @property bool $useMarkupRegions Enable support for front-end markup regions? #pw-group-system
* @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
@@ -128,30 +143,36 @@
* @property int $inputfieldColumnWidthSpacing Used by some admin themes to commmunicate to InputfieldWrapper at runtime. #pw-internal
* @property bool $debugMarkupQA Set to true to make the MarkupQA class report verbose debugging messages (to superusers). #pw-internal
*
* @property int $rootPageID ID of homepage (usually 1) #pw-group-system-IDs
* @property int $adminRootPageID ID of admin root page #pw-group-system-IDs
* @property int $trashPageID #pw-group-system-IDs
* @property int $loginPageID #pw-group-system-IDs
* @property int $http404PageID #pw-group-system-IDs
* @property int $usersPageID #pw-group-system-IDs
* @property int $rootPageID Page ID of homepage (usually 1) #pw-group-system-IDs
* @property int $adminRootPageID Page ID of admin root page #pw-group-system-IDs
* @property int $trashPageID Page ID of the trash page. #pw-group-system-IDs
* @property int $loginPageID Page ID of the admin login page. #pw-group-system-IDs
* @property int $http404PageID Page ID of the 404 “page not found” page. #pw-group-system-IDs
* @property int $usersPageID Page ID of the page having users as children. #pw-group-system-IDs
* @property array $usersPageIDs Populated if multiple possible users page IDs (parent for users pages) #pw-group-system-IDs
* @property int $rolesPageID #pw-group-system-IDs
* @property int $permissionsPageID #pw-group-system-IDs
* @property int $guestUserPageID #pw-group-system-IDs
* @property int $superUserPageID #pw-group-system-IDs
* @property int $guestUserRolePageID #pw-group-system-IDs
* @property int $superUserRolePageID #pw-group-system-IDs
* @property int $userTemplateID #pw-group-system-IDs
* @property int $rolesPageID Page ID of the page having roles as children. #pw-group-system-IDs
* @property int $permissionsPageID Page ID of the page having permissions as children. #pw-group-system-IDs
* @property int $guestUserPageID Page ID of the guest (default/not-logged-in) user. #pw-group-system-IDs
* @property int $superUserPageID Page ID of the original superuser (created during installation). #pw-group-system-IDs
* @property int $guestUserRolePageID Page ID of the guest user role (inherited by all users, not just guest). #pw-group-system-IDs
* @property int $superUserRolePageID Page ID of the superuser role. #pw-group-system-IDs
* @property int $userTemplateID Template ID of the user template. #pw-group-system-IDs
* @property array $userTemplateIDs Array of template IDs when multiple allowed for users. #pw-group-system-IDs
* @property int $roleTemplateID #pw-group-system-IDs
* @property int $permissionTemplateID #pw-group-system-IDs
* @property int $externalPageID ID of page assigned to $page API variable when externally bootstrapped #pw-group-system-IDs
* @property array $preloadPageIDs IDs of pages that will be preloaded at beginning of request #pw-group-system-IDs
* @property int $installed Timestamp of when this PW was installed, set automatically for compatibility detection. #pw-group-system
* @property int $roleTemplateID Template ID of the role template. #pw-group-system-IDs
* @property int $permissionTemplateID Template ID of the permission template. #pw-group-system-IDs
* @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
*
*/
class Config extends WireData {
/**
* Constant for verbose debug mode (uses more memory)
*
*/
const debugVerbose = 2;
/**
* Get URL for requested resource or module
*
@@ -165,7 +186,7 @@ class Config extends WireData {
* $url = $config->urls->admin;
* ~~~~~
*
* @param string $for Predefined ProcessWire URLs property or module name
* @param string|Wire $for Predefined ProcessWire URLs property or module name
* @return string|null
*
*/
@@ -178,7 +199,7 @@ class Config extends WireData {
*
* #pw-internal
*
* @param string $for Predefined ProcessWire URLs property or module name
* @param string|Wire $for Predefined ProcessWire URLs property or module name
* @return null|string
*
*/
@@ -214,7 +235,7 @@ class Config extends WireData {
* @return null|string
*
*/
public function paths($for) { return $this->paths($for); }
public function paths($for) { return $this->path($for); }
/**
* List of config keys that are also exported in javascript
@@ -327,6 +348,9 @@ class Config extends WireData {
* 'siteOnly' => true,
* 'cachePath' => $config->paths->root . '.my-cache/'
* ]);
*
* // To unset a property specify null for first argument and property to unset as second argument
* $config->fileCompilerOptions(null, 'siteOnly');
* ~~~~~
*
* #pw-internal
@@ -361,7 +385,14 @@ class Config extends WireData {
}
} else {
// property and value provided
$value[$property] = $arguments[1];
if($property === null && is_string($arguments[1])) {
// unset property
$property = $arguments[1];
unset($value[$property]);
} else {
// set property with value
$value[$property] = $arguments[1];
}
parent::set($method, $value);
}
} else if($numArgs === 1) {

View File

@@ -104,7 +104,7 @@ class Database extends \mysqli implements WireDatabase {
if($this->debug) {
$timerKey = Debug::timer();
if(!$timerFirstStartTime) $timerFirstStartTime = $timerKey;
if(!$timerFirstStartTime) $timerFirstStartTime = (float) $timerKey;
} else $timerKey = null;
$result = @parent::query($sql, $resultmode);
@@ -113,9 +113,9 @@ class Database extends \mysqli implements WireDatabase {
if($this->debug) {
if(isset($result->num_rows)) $sql .= " [" . $result->num_rows . " rows]";
if(!is_null($timerKey)) {
$elapsed = Debug::timer($timerKey);
$elapsed = (float) Debug::timer($timerKey);
$timerTotalQueryTime += $elapsed;
$timerTotalSinceStart = Debug::timer() - $timerFirstStartTime;
$timerTotalSinceStart = ((float) Debug::timer()) - $timerFirstStartTime;
$sql .= " [{$elapsed}s, {$timerTotalQueryTime}s, {$timerTotalSinceStart}s]";
}
$this->queryLog($sql);

View File

@@ -66,7 +66,7 @@ class Debug {
$startTime = -microtime(true);
if(!$key) {
$key = (string) $startTime;
while(isset(self::$timers[$key])) $key .= ".";
while(isset(self::$timers[$key])) $key .= "0";
}
self::$timers[(string) $key] = $startTime;
$value = $key;

View File

@@ -7,6 +7,8 @@
* and is managed by the 'Fields' class.
*
* #pw-summary Field represents a custom field that is used on a Page.
* #pw-var $field
* #pw-instantiate $field = $fields->get('field_name');
* #pw-body Field objects are managed by the `$fields` API variable.
* #pw-use-constants
*
@@ -29,12 +31,13 @@
* @property array $viewRoles Role IDs with view access, applicable only if access control is enabled. #pw-group-access
* @property array|null $orderByCols Columns that WireArray values are sorted by (default=null), Example: "sort" or "-created". #pw-internal
* @property int|null $paginationLimit Used by paginated WireArray values to indicate limit to use during load. #pw-internal
* @property array $allowContexts Names of settings that are custom configured to be allowed for context. #pw-group-properties
*
* Common Inputfield properties that Field objects store:
* @property int|bool|null $required
* @property string|null $requiredIf
* @property string|null $showIf
* @property int|null $columnWidth
* @property int|bool|null $required Whether or not this field is required during input #pw-group-properties
* @property string|null $requiredIf A selector-style string that defines the conditions under which input is required #pw-group-properties
* @property string|null $showIf A selector-style string that defines the conditions under which the Inputfield is shown #pw-group-properties
* @property int|null $columnWidth The Inputfield column width (percent) 10-100. #pw-group-properties
*
* @method bool viewable(Page $page = null, User $user = null) Is the field viewable on the given $page by the given $user? #pw-group-access
* @method bool editable(Page $page = null, User $user = null) Is the field editable on the given $page by the given $user? #pw-group-access
@@ -216,7 +219,7 @@ class Field extends WireData implements Saveable, Exportable {
*
* @param string $key Property name to set
* @param mixed $value
* @return $this
* @return Field|WireData
*
*/
public function set($key, $value) {
@@ -339,16 +342,17 @@ class Field extends WireData implements Saveable, Exportable {
*/
public function get($key) {
if($key == 'viewRoles') return $this->viewRoles;
else if($key == 'editRoles') return $this->editRoles;
else if($key == 'table') return $this->getTable();
else if($key == 'prevTable') return $this->prevTable;
else if($key == 'prevFieldtype') return $this->prevFieldtype;
else if(isset($this->settings[$key])) return $this->settings[$key];
else if($key == 'icon') return $this->getIcon(true);
else if($key == 'useRoles') return ($this->settings['flags'] & self::flagAccess) ? true : false;
else if($key == 'flags') return $this->settings['flags'];
else if($key == 'editRoles') return $this->editRoles;
else if($key == 'table') return $this->getTable();
else if($key == 'prevTable') return $this->prevTable;
else if($key == 'prevFieldtype') return $this->prevFieldtype;
else if(isset($this->settings[$key])) return $this->settings[$key];
else if($key == 'icon') return $this->getIcon(true);
else if($key == 'useRoles') return ($this->settings['flags'] & self::flagAccess) ? true : false;
else if($key == 'flags') return $this->settings['flags'];
$value = parent::get($key);
if($key === 'allowContexts' && !is_array($value)) $value = array();
if(is_array($this->trackGets)) $this->trackGets($key);
return $value;
}
@@ -444,6 +448,20 @@ class Field extends WireData implements Saveable, Exportable {
if(strpos($key, '_') === 0) unset($data[$key]);
}
// convert access roles from IDs to names
if($this->useRoles) {
foreach(array('viewRoles', 'editRoles') as $roleType) {
if(!is_array($data[$roleType])) $data[$roleType] = array();
$roleNames = array();
foreach($data[$roleType] as $key => $roleID) {
$role = $this->wire('roles')->get($roleID);
if(!$role || !$role->id) continue;
$roleNames[] = $role->name;
}
$data[$roleType] = $roleNames;
}
}
return $data;
}
@@ -493,7 +511,11 @@ class Field extends WireData implements Saveable, Exportable {
$this->type = $this->wire('fieldtypes')->get($data['type']);
}
if(!$this->type) $this->type = $this->wire('fieldtypes')->get('FieldtypeText');
if(!$this->type) {
if(!empty($data['type'])) $this->error("Unable to locate field type: $data[type]");
$this->type = $this->wire('fieldtypes')->get('FieldtypeText');
}
$data = $this->type->importConfigData($this, $data);
// populate import data
@@ -628,6 +650,15 @@ class Field extends WireData implements Saveable, Exportable {
$ids[] = (int) $role;
} else if($role instanceof Role) {
$ids[] = (int) $role->id;
} else if(is_string($role) && strlen($role)) {
$rolePage = $this->wire('roles')->get($role);
if($rolePage && $rolePage->id) {
$ids[] = $rolePage->id;
} else {
$this->error("Unknown role '$role'");
}
} else {
// invalid
}
}
if($type == 'view') {
@@ -808,8 +839,10 @@ class Field extends WireData implements Saveable, Exportable {
$inputfield->attr('name', $this->name . $contextStr);
$inputfield->set('label', $this->label);
// just in case an Inputfield needs to know it's Fieldtype context, or lack of it
$inputfield->set('hasFieldtype', $this->type);
// just in case an Inputfield needs to know its Fieldtype/Field context, or lack of it
$inputfield->set('hasFieldtype', $this->type);
$inputfield->set('hasField', $this);
$inputfield->set('hasPage', $page);
// custom field settings
foreach($this->data as $key => $value) {
@@ -912,6 +945,7 @@ class Field extends WireData implements Saveable, Exportable {
if($fieldgroupContext) {
$allowContext = $this->type->getConfigAllowContext($this);
if(!is_array($allowContext)) $allowContext = array();
$allowContext = array_merge($allowContext, $this->allowContexts);
} else {
$allowContext = array();
}
@@ -922,6 +956,8 @@ class Field extends WireData implements Saveable, Exportable {
if(!$fieldgroupContext) $inputfields->head = $this->_('Field type details');
$inputfields->attr('title', $this->_('Details'));
$inputfields->attr('id+name', 'fieldtypeConfig');
$remainingNames = array();
foreach($allowContext as $name) $remainingNames[$name] = $name;
try {
$fieldtypeInputfields = $this->type->getConfigInputfields($this);
@@ -936,7 +972,19 @@ class Field extends WireData implements Saveable, Exportable {
foreach($fieldtypeInputfields as $inputfield) {
if($fieldgroupContext && !in_array($inputfield->name, $allowContext)) continue;
$inputfields->append($inputfield);
unset($remainingNames[$inputfield->name]);
}
// now capture those that may have been stuck in a fieldset
if($fieldgroupContext) {
foreach($remainingNames as $name) {
if($inputfields->getChildByName($name)) continue;
$inputfield = $fieldtypeInputfields->getChildByName($name);
if(!$inputfield) continue;
$inputfields->append($inputfield);
unset($remainingNames[$inputfield->name]);
}
}
} catch(\Exception $e) {
$this->trackException($e, false, true);
}
@@ -951,11 +999,15 @@ class Field extends WireData implements Saveable, Exportable {
if($inputfield) {
if($fieldgroupContext) {
$allowContext = array('visibility', 'collapsed', 'columnWidth', 'required', 'requiredIf', 'showIf');
$allowContext = array_merge($allowContext, $inputfield->getConfigAllowContext($this));
$allowContext = array_merge($allowContext, $this->allowContexts, $inputfield->getConfigAllowContext($this));
} else {
$allowContext = array();
$inputfields->head = $this->_('Input field settings');
}
$remainingNames = array();
foreach($allowContext as $name) {
$remainingNames[$name] = $name;
}
$inputfields->attr('title', $this->_('Input'));
$inputfields->attr('id+name', 'inputfieldConfig');
$inputfieldInputfields = $inputfield->getConfigInputfields();
@@ -970,6 +1022,16 @@ class Field extends WireData implements Saveable, Exportable {
foreach($inputfieldInputfields as $i) {
if($fieldgroupContext && !in_array($i->name, $allowContext)) continue;
$inputfields->append($i);
unset($remainingNames[$i->name]);
}
if($fieldgroupContext) {
foreach($remainingNames as $name) {
if($inputfields->getChildByName($name)) continue;
$inputfield = $inputfieldInputfields->getChildByName($name);
if(!$inputfield) continue;
$inputfields->append($inputfield);
unset($remainingNames[$inputfield->name]);
}
}
}
@@ -1003,6 +1065,8 @@ class Field extends WireData implements Saveable, Exportable {
/**
* Set an override table name, or omit (or null) to restore default table name
*
* #pw-group-advanced
*
* @param null|string $table
*
*/

View File

@@ -133,7 +133,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
/**
* Remove a field from this fieldgroup
*
* Note that this must be followed up with a `$field->save()` before it does anything destructive.
* Note that this must be followed up with a `$fieldgroup->save()` before it does anything destructive.
* This method does nothing more than queue the removal.
*
* _Technical Details_
@@ -177,7 +177,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
* #pw-internal
*
* @param Field $field
* @return bool
* @return Fieldgroup|WireArray $this
*
*/
public function finishRemove(Field $field) {
@@ -194,7 +194,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
* #pw-group-manipulation
*
* @param Field|string|int $field Field object, name or id.
* @return bool
* @return bool|Fieldgroup|WireArray
*
*/
public function softRemove($field) {
@@ -379,7 +379,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
*
* @param string $key Name of property to set
* @param string|int|object $value Value of property
* @return Fieldgroup $this
* @return Fieldgroup|WireArray $this
* @throws WireException if passed invalid data
*
*/
@@ -448,7 +448,9 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
*
*/
public function getExportData() {
return $this->wire('fieldgroups')->getExportData($this);
/** @var Fieldgroups $fieldgroups */
$fieldgroups = $this->wire('fieldgroups');
return $fieldgroups->getExportData($this);
}
/**
@@ -469,7 +471,9 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
*
*/
public function setImportData(array $data) {
return $this->wire('fieldgroups')->setImportData($this, $data);
/** @var Fieldgroups $fieldgroups */
$fieldgroups = $this->wire('fieldgroups');
return $fieldgroups->setImportData($this, $data);
}
/**
@@ -612,7 +616,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
if(!$inputfield) continue;
if($inputfield->collapsed == Inputfield::collapsedHidden) continue;
$inputfield->value = $page->get($field->name);
$inputfield->setAttribute('value', $page->get($field->name));
if($multiMode) {
$fieldInputfields[$field->id] = $inputfield;
@@ -640,7 +644,9 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
*
*/
public function getTemplates() {
return $this->wire('fieldgroups')->getTemplates($this);
/** @var Fieldgroups $fieldgroups */
$fieldgroups = $this->wire('fieldgroups');
return $fieldgroups->getTemplates($this);
}
/**
@@ -686,7 +692,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
return $this->fieldContexts[$field_id][$namespace];
}
return array();
} else {
} else if(isset($this->fieldContexts[$field_id])) {
return $this->fieldContexts[$field_id];
}
}
@@ -699,8 +705,8 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
* #pw-internal
*
* @param int $field_id Field ID
* @param string $namespace Optional namespace
* @param array $data
* @param string $namespace Optional namespace
*
*/
public function setFieldContextArray($field_id, $data, $namespace = '') {

View File

@@ -147,21 +147,12 @@ class Fieldgroups extends WireSaveableItemsLookup {
if($item->id && $item->removedFields) {
foreach($this->wire('templates') as $template) {
if($template->fieldgroup->id !== $item->id) continue;
foreach($item->removedFields as $field) {
// make sure the field is valid to delete from this template
if(($field->flags & Field::flagGlobal) && !$template->noGlobal) {
throw new WireException("Field '$field' may not be removed from fieldgroup '{$item->name}' because it is globally required (Field::flagGlobal)");
}
if($field->flags & Field::flagPermanent) {
throw new WireException("Field '$field' may not be removed from fieldgroup '{$item->name}' because it is permanent.");
}
$field->type->deleteTemplateField($template, $field);
$error = $this->isFieldNotRemoveable($field, $item, $template);
if($error !== false) throw new WireException("$error Save of fieldgroup changes aborted.");
if($field->type) $field->type->deleteTemplateField($template, $field);
$item->finishRemove($field);
}
}
@@ -206,7 +197,7 @@ class Fieldgroups extends WireSaveableItemsLookup {
* Also deletes the references in fieldgroups_fields table
*
* @param Saveable|Fieldgroup $item
* @return Fieldgroups $this
* @return bool
* @throws WireException
*
*/
@@ -218,7 +209,10 @@ class Fieldgroups extends WireSaveableItemsLookup {
}
if(count($templates)) {
throw new WireException("Can't delete fieldgroup '{$item->name}' because it is in use by template(s): " . implode(', ', $templates));
throw new WireException(
"Can't delete fieldgroup '{$item->name}' because it is in use by template(s): " .
implode(', ', $templates)
);
}
return parent::___delete($item);
@@ -272,7 +266,8 @@ class Fieldgroups extends WireSaveableItemsLookup {
$contexts = $fieldgroup->getFieldContextArray();
$numSaved = 0;
foreach($contexts as $fieldID => $context) {
$field = $fieldgroup->getFieldContext($fieldID);
$field = $fieldgroup->getFieldContext((int) $fieldID);
if(!$field) continue;
if($this->wire('fields')->saveFieldgroupContext($field, $fieldgroup)) $numSaved++;
}
return $numSaved;
@@ -454,5 +449,35 @@ class Fieldgroups extends WireSaveableItemsLookup {
return $return;
}
/**
* Is the given Field not allowed to be removed from given Template?
*
* #pw-internal
*
* @param Field $field
* @param Template $template
* @param Fieldgroup $fieldgroup
* @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) {
if(is_null($template)) $template = $this->wire('templates')->get($fieldgroup->name);
if(($field->flags & Field::flagGlobal) && (!$template || !$template->noGlobal)) {
return
"Field '$field' may not be removed from fieldgroup '{$this->name}' " .
"because it is globally required (Field::flagGlobal).";
}
if($field->flags & Field::flagPermanent) {
return
"Field '$field' may not be removed from fieldgroup '{$this->name}' " .
"because it is permanent (Field::flagPermanent).";
}
return false;
}
}

View File

@@ -16,6 +16,7 @@
* @method bool deleteFieldDataByTemplate(Field $field, Template $template) #pw-hooker
* @method void changedType(Saveable $item, Fieldtype $fromType, Fieldtype $toType) #pw-hooker
* @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.
*
*/
@@ -34,47 +35,48 @@ class Fields extends WireSaveableItems {
*
*/
static protected $nativeNamesSystem = array(
'id',
'parent_id',
'parent', // alias
'parents',
'templates_id',
'template', // alias
'name',
'status',
'child',
'children',
'created_users_id',
'created',
'createdUser',
'createdUser',
'createdUserID',
'createdUsersID',
'created_users_id',
'data',
'description',
'fieldgroup',
'fields',
'find',
'flags',
'get',
'id',
'include',
'modified',
'modifiedUser',
'isNew',
'limit',
'modified_users_id',
'modified',
'modifiedUser',
'modifiedUserID',
'modifiedUsersID',
'modified_users_id',
'published',
'name',
'num_children',
'numChildren',
'sort',
'sortfield',
'flags',
'find',
'get',
'child',
'children',
'siblings',
//'roles',
'url',
'path',
'templatePrevious',
'rootParent',
'fieldgroup',
'fields',
'description',
'data',
'isNew',
);
'numChildren',
'parent_id',
'parent',
'parents',
'path',
'published',
'rootParent',
'siblings',
'sort',
'sortfield',
'start',
'status',
'template',
'templatePrevious',
'templates_id',
'url',
);
/**
* Field names that are native/permanent to this instance of ProcessWire (configurable at runtime)
@@ -281,7 +283,7 @@ class Fields extends WireSaveableItems {
/**
* Create and return a cloned copy of the given Field
*
*
* @param Field|Saveable $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
@@ -622,14 +624,14 @@ class Fields extends WireSaveableItems {
if($success) {
$this->message(
sprintf($this->_('Deleted field "%1$s" data in %2$d row(s) from %3$d page(s).'),
$field->name, $numRows, $numPages) . " [$deleteType]",
sprintf($this->_('Deleted field "%1$s" data in %2$d row(s) from %3$d page(s) using template "%4$s".'),
$field->name, $numRows, $numPages, $template->name) . " [$deleteType]",
Notice::log
);
} else {
$this->error(
sprintf($this->_('Error deleting field "%1$s" data, %2$d row(s), %3$d page(s).'),
$field->name, $numRows, $numPages) . " [$deleteType]",
sprintf($this->_('Error deleting field "%1$s" data, %2$d row(s), %3$d page(s) using template "%4$s".'),
$field->name, $numRows, $numPages, $template->name) . " [$deleteType]",
Notice::log
);
}
@@ -676,6 +678,8 @@ class Fields extends WireSaveableItems {
'countPages' => false,
'getPageIDs' => false,
);
if(!$field->type) return 0;
$options = array_merge($defaults, $options);
$database = $this->wire('database');

View File

@@ -33,6 +33,7 @@
* @method mixed wakeupValue(Page $page, Field $field, $value)
* @method string|int|array sleepValue(Page $page, Field $field, $value)
* @method string|float|int|array exportValue(Page $page, Field $field, $value, array $options = array())
* @method string|float|int|array|object importValue(Page $page, Field $field, $value, array $options = array())
* @method bool createField(Field $field)
* @method array getSelectorInfo(Field $field, array $data = array())
* @method mixed|null loadPageField(Page $page, Field $field)
@@ -51,6 +52,7 @@
* @property bool $_exportMode True when Fieldtype is exporting config data, false otherwise. #pw-internal
* @property string $name Name of Fieldtype module. #pw-group-other
* @property string $shortName Short name of Fieldtype, which excludes the "Fieldtype" prefix. #pw-group-other
* @property string $longName Long name of Fieldtype, which is typically the module title. #pw-group-other
*
*/
abstract class Fieldtype extends WireData implements Module {
@@ -396,13 +398,11 @@ abstract class Fieldtype extends WireData implements Module {
* need to have some text formatting applied to them, like Markdown, SmartyPants, Textile, etc. As a result,
* Fieldtype modules don't need to implement this unless it's applicable.
*
* Fieldtype modules that implement this do not need to call this parent method, as it doesn't do anything.
*
* #pw-group-formatting
*
* @param Page $page
* @param Field $field
* @param string|int|object $value
* @param Page $page Page that the value lives on
* @param Field $field Field that represents the value
* @param string|int|object $value The value to format
* @return mixed
*
*/
@@ -522,6 +522,7 @@ abstract class Fieldtype extends WireData implements Module {
* @param Field $field
* @param string|int|float|array|object $value
* @return string|int|float|array
* @see Fieldtype::wakeupValue()
*
*/
public function ___sleepValue(Page $page, Field $field, $value) {
@@ -532,24 +533,69 @@ abstract class Fieldtype extends WireData implements Module {
/**
* Given a value originally generated by exportValue() convert it to a live/runtime value.
*
* This is intended for importing from PW-driven web services.
* This is intended for importing from PW-driven web services. If not overridden, it does
* the same thing as the `Fieldtype::wakeupValue()` method.
*
* #pw-internal
*
* @param Page $page
* @param Field $field
* @param string|int|array $value
* @param string|int|float|array|null $value
* @param array $options Additional options if needed/applicable
* @return string|int|array|object $value
* @see Fieldtype::exportValue()
*
public function ___importValue(Page $page, Field $field, $value) {
*/
public function ___importValue(Page $page, Field $field, $value, array $options = array()) {
if($options) {}
$value = $this->wakeupValue($page, $field, $value);
return $value;
}
/**
* Get associative array of options and info (name => value) that Fieldtype supports for importValue
*
* Current recognized options include the following:
*
* - `importable` (bool): Is the field importable (and exportable)? (default=auto-detect)
*
* - `test` (bool): Indicates Fieldtype supports testing import before committing & populates notices to
* returned Wire object. (default=false)
*
* - `returnsPageValue` (bool): True if it returns the value that should set back to Page? False if return
* value should not be set to Page. When false, it indicates the Fieldtype::importValue() handles the
* actual commit to DB of import data. (default=true)
*
* - `requiresExportValue` (bool): Indicates Fieldtype::importValue() requires an 'exportValue' of the
* current value from Page in $options. (default=false)
*
* - `restoreOnException` (bool): Restore previous value if Exception thrown during import (default=false).
*
* #pw-internal
*
* @param array Field $field
* @return array
*
*/
public function getImportValueOptions(Field $field) {
$schema = $this->getDatabaseSchema($field);
$options = array(
'importable' => (!isset($schema['xtra']['all']) || $schema['xtra']['all'] !== true) ? false : true,
'test' => false,
'returnsPageValue' => true,
'requiresExportValue' => false,
'restoreOnException' => false,
);
return $options;
}
/**
* Given a value, return an portable version of it as either a string, int, float or array
*
* If an array is returned, it should only contain: strings, ints, floats or more arrays of those types.
* This is intended for web service exports.
*
* When applicable, this method should map things like internal IDs to named equivalents (name, path, etc.).
*
* If not overridden, this takes on the same behavior as `Fieldtype::sleepValue()`. However, if overridden,
* it is intended to be more verbose than wakeupValue, where applicable.
@@ -558,8 +604,10 @@ abstract class Fieldtype extends WireData implements Module {
*
* @param Page $page
* @param Field $field
* @param string|int|array|object $value
* @param array $options Optional settings to shape the exported value, if needed.
* @param string|int|float|array|object|null $value
* @param array $options Optional settings to shape the exported value, if needed:
* - `system` (boolean): Indicates value is being used for a system export via $pages->export() call (default=false).
* - `human` (boolean): When true, Fieldtype may optionally emphasize human readability over importability (default=false).
* @return string|float|int|array
*
*/
@@ -1259,7 +1307,12 @@ abstract class Fieldtype extends WireData implements Module {
*/
public function get($key) {
if($key == 'name') return $this->className();
if($key == 'shortName') return str_replace('Fieldtype', '', $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'];
}
return parent::get($key);
}

View File

@@ -798,11 +798,16 @@ abstract class FieldtypeMulti extends Fieldtype {
}
// only allow matches using templates with the requested field
$sql = 'pages.templates_id IN(';
foreach($field->getTemplates() as $template) {
$sql .= ((int) $template->id) . ',';
$templates = $field->getTemplates();
if(count($templates)) {
$sql = 'pages.templates_id IN(';
foreach($templates as $template) {
$sql .= ((int) $template->id) . ',';
}
$sql = rtrim($sql, ',') . ')';
} else {
$sql = 'pages.templates_id=0';
}
$sql = rtrim($sql, ',') . ')';
$query->where($sql); // QA
} else {

View File

@@ -99,6 +99,22 @@ class FileCompiler extends Wire {
*/
protected $ns = '';
/**
* String with raw PHP blocks only, and with any quoted values removed.
*
* @var string
*
*/
protected $rawPHP = '';
/**
* Same as raw PHP but with all quoted values converted to literal "string"
*
* @var string
*
*/
protected $rawDequotedPHP = '';
/**
* Construct
*
@@ -214,6 +230,52 @@ class FileCompiler extends Wire {
}
}
/**
* Populate the $this->rawPHP data which contains only raw php without quoted values
*
* @param string $data
*
*/
protected function initRawPHP(&$data) {
$this->rawPHP = '';
$this->rawDequotedPHP = '';
$phpOpen = '<' . '?';
$phpClose = '?' . '>';
$phpBlocks = explode($phpOpen, $data);
foreach($phpBlocks as $key => $phpBlock) {
$pos = strpos($phpBlock, $phpClose);
if($pos !== false) {
$closeBlock = substr($phpBlock, strlen($phpClose) + 2);
if(strrpos($closeBlock, '{') && strrpos($closeBlock, '}') && strrpos($closeBlock, '=')
&& strrpos($closeBlock, '(') && strrpos($closeBlock, ')')
&& preg_match('/\sif\s*\(/', $closeBlock)
&& preg_match('/\$[_a-zA-Z][_a-zA-Z0-9]+/', $closeBlock)) {
// closeBlock still looks a lot like PHP, leave $phpBlock as-is
// happens when for example a phpClose is within a PHP string
} else {
$phpBlock = substr($phpBlock, 0, $pos);
}
}
$this->rawPHP .= $phpOpen . $phpBlock . $phpClose . "\n";
}
// remove docblocks/comments
// $this->rawPHP = preg_replace('!/\*.+?\*/!s', '', $this->rawPHP);
// remove escaped quotes
$this->rawDequotedPHP = str_replace(array('\\"', "\\'"), '', $this->rawPHP);
// remove double quoted blocks
$this->rawDequotedPHP = preg_replace('/([\s(.=,])"[^"]*"/s', '$1"string"', $this->rawDequotedPHP);
// remove single quoted blocks
$this->rawDequotedPHP = preg_replace('/([\s(.=,])\'[^\']*\'/s', '$1\'string\'', $this->rawDequotedPHP);
}
/**
* Allow the given filename to be compiled?
*
@@ -384,9 +446,13 @@ class FileCompiler extends Wire {
// file already declares a namespace and options indicate we shouldn't compile
return $data;
}
$this->initRawPHP($data);
if($this->options['includes']) {
$dataHash = md5($data);
$this->compileIncludes($data, $sourceFile);
if(md5($data) != $dataHash) $this->initRawPHP($data);
}
if($this->options['namespace']) {
@@ -424,11 +490,16 @@ class FileCompiler extends Wire {
}
if(!strlen(__NAMESPACE__)) {
if(strpos($data, "ProcessWire\\")) {
if(strpos($this->rawPHP, "ProcessWire\\")) {
$data = str_replace(array("\\ProcessWire\\", "ProcessWire\\"), "\\", $data);
}
}
if(stripos($data, "FileCompiler=?") !== false) {
// Allow for a token that gets replaced so a file can detect if it's compiled
$data = str_replace("FileCompiler=?", "FileCompiler=Yes", $data);
}
return $data;
}
@@ -507,11 +578,14 @@ class FileCompiler extends Wire {
protected function compileIncludes(&$data, $sourceFile) {
// other related to includes
if(strpos($data, '__DIR__') !== false) {
$rawPHP = $this->rawPHP;
if(strpos($rawPHP, '__DIR__') !== false) {
$data = str_replace('__DIR__', "'" . dirname($sourceFile) . "'", $data);
$rawPHP = str_replace('__DIR__', "'" . dirname($sourceFile) . "'", $rawPHP);
}
if(strpos($data, '__FILE__') !== false) {
if(strpos($rawPHP, '__FILE__') !== false) {
$data = str_replace('__FILE__', "'" . $sourceFile . "'", $data);
$rawPHP = str_replace('__FILE__', "'" . $sourceFile . "'", $rawPHP);
}
$optionsStr = $this->optionsToString($this->options);
@@ -535,10 +609,15 @@ class FileCompiler extends Wire {
'([;\r\n])' . // 5:close, whatever the last character is on the line
'/im';
if(!preg_match_all($re, $data, $matches)) return;
if(!preg_match_all($re, $rawPHP, $matches)) return;
foreach($matches[0] as $key => $fullMatch) {
// if the include statement looks like one of these below then skip compilation for included file
// include(/*NoCompile*/__DIR__ . '/file.php');
// include(__DIR__ . '/file.php'/*NoCompile*/);
if(strpos($fullMatch, 'NoCompile') !== false) continue;
$open = $matches[1][$key];
$funcMatch = $matches[2][$key];
$argOpen = trim($matches[3][$key]);
@@ -550,45 +629,10 @@ class FileCompiler extends Wire {
// only include, include_once, require, require_once can be used without opening parenthesis
continue;
}
if(strpos($fileMatch, '$') === 0) {
// fileMatch stars with a var name
} else if(strpos($fileMatch, '"') !== strrpos($fileMatch, '"')) {
// fileMatch has both open and close double quotes
} else if(strpos($fileMatch, "'") !== strrpos($fileMatch, "'")) {
// fileMatch has both open and close single quotes
} else if(strpos($fileMatch, '(') !== false && strpos($fileMatch, ')') !== false) {
// likely a function call
} else {
// likely NOT a valid file match, as it doesn't have any of the expected characters
continue;
}
if(strlen($open)) {
$skipMatch = false;
$test = $open;
foreach(array('"', "'") as $quote) {
// skip when words like "require" are in a string
if(strpos($test, $quote) === false) continue;
$test = str_replace('\\' . $quote, '', $test); // ignore quotes that are escaped
if(strpos($test, $quote) === false) continue;
if(substr_count($test, $quote) % 2 > 0) {
// there are an uneven number of quotes, indicating that
// our $funcMatch is likely part of a quoted string
$skipMatch = true;
break;
}
if($quote == '"' && strpos($test, "'") !== false) {
// remove quoted apostrophes so they don't confuse the next iteration
$test = preg_replace('/"[^"\']*\'[^"]*"/', '', $test);
}
}
if($skipMatch) continue;
if(preg_match('/^[$_a-zA-Z0-9]+$/', substr($open, -1))) {
// skip things like: something_include(... and $include
continue;
}
}
$fileMatchType = $this->compileIncludesFileMatchType($fileMatch, $funcMatch);
if(!$fileMatchType) continue;
if(!$this->compileIncludesValidLineOpen($open)) continue;
if(strpos($fileMatch, '?' . '>')) {
// move closing PHP tag out of the fileMatch and into the close
@@ -641,14 +685,6 @@ class FileCompiler extends Wire {
// replace absolute root path references with runtime generated versions
$rootPath = $this->wire('config')->paths->root;
if(strpos($data, $rootPath)) {
/*
$data = preg_replace('%([\'"])' . preg_quote($rootPath) . '([^\'"\s\r\n]*[\'"])%',
'(isset($this) && $this instanceof \\ProcessWire\\Wire ? ' .
'$this->wire("config")->paths->root : ' .
'\\ProcessWire\\wire("config")->paths->root' .
') . $1$2',
$data);
*/
$ns = __NAMESPACE__ ? "\\ProcessWire" : "";
$data = preg_replace('%([\'"])' . preg_quote($rootPath) . '([^\'"\s\r\n]*[\'"])%',
$ns . '\\wire("config")->paths->root . $1$2',
@@ -657,6 +693,98 @@ class FileCompiler extends Wire {
}
/**
* Test the given line $open preceding an include statement for validity
*
* @param string $open
* @return bool Returns true if valid, false if not
*
*/
protected function compileIncludesValidLineOpen($open) {
if(!strlen($open)) return true;
$skipMatch = false;
$test = $open;
foreach(array('"', "'") as $quote) {
// skip when words like "require" are in a string
if(strpos($test, $quote) === false) continue;
$test = str_replace('\\' . $quote, '', $test); // ignore quotes that are escaped
if(strpos($test, $quote) === false) continue;
if(substr_count($test, $quote) % 2 > 0) {
// there are an uneven number of quotes, indicating that
// our $funcMatch is likely part of a quoted string
$skipMatch = true;
break;
}
if($quote == '"' && strpos($test, "'") !== false) {
// remove quoted apostrophes so they don't confuse the next iteration
$test = preg_replace('/"[^"\']*\'[^"]*"/', '', $test);
}
}
if(!$skipMatch && preg_match('/^[$_a-zA-Z0-9]+$/', substr($open, -1))) {
// skip things like: something_include(... and $include
$skipMatch = true;
}
return $skipMatch ? false : true;
}
/**
* Returns fileMatch type of 'var', 'file', 'func' or boolean false if not valid
*
* @param string $fileMatch The $fileMatch var from compileIncludes() method
* @param string $funcMatch include function name
* @return string|bool
*
*/
protected function compileIncludesFileMatchType($fileMatch, $funcMatch) {
$fileMatch = trim($fileMatch);
$isValid = false;
$phpVarSign = strpos($fileMatch, '$');
$doubleQuote1 = strpos($fileMatch, '"');
$doubleQuote2 = strrpos($fileMatch, '"');
$singleQuote1 = strpos($fileMatch, "'");
$singleQuote2 = strrpos($fileMatch, "'");
$parenthesis1 = strpos($fileMatch, '(');
$parenthesis2 = strrpos($fileMatch, ')');
$testFile = '';
if($phpVarSign === 0) {
// fileMatch starts with a var name, make sure it at least starts in PHP var format
if(preg_match('/^\$[_a-zA-Z]/', $fileMatch)) $isValid = 'var';
} else if($doubleQuote1 !== false && $doubleQuote2 > $doubleQuote1) {
// fileMatch has both open and close double quotes with possibly a filename, so validate extension
$testFile = substr($fileMatch, $doubleQuote1 + 1, $doubleQuote2 - $doubleQuote1 - 1);
} else if($singleQuote1 !== false && $singleQuote2 > $singleQuote1) {
// fileMatch has both open and close single quotes with possibly a filename, so validate extension
$testFile = substr($fileMatch, $singleQuote1 + 1, $singleQuote2 - $singleQuote1 - 1);
} else if($parenthesis1 > 0 && $parenthesis2 > $parenthesis1) {
// likely a function call, make sure open parenthesis is preceded by PHP name format
if(preg_match('/[_a-zA-Z][_a-zA-Z0-9]+\(/', $fileMatch)) $isValid = 'func';
} else {
// likely NOT a valid file match, as it doesn't have any of the expected characters
$isValid = false;
}
if($testFile) {
if(strrpos($testFile, '.')) {
// test contains a filename that needs extension validated
$parts = explode('.', $testFile);
$testExt = array_pop($parts);
if($testExt && in_array(strtolower($testExt), $this->extensions)) $isValid = 'file';
} else if($funcMatch == 'wireRenderFile' || $funcMatch == 'wireIncludeFile') {
// these methods don't require a file extension
$isValid = 'file';
}
}
return $isValid;
}
/**
* Compile global class/interface/function references to namespaced versions
*
@@ -677,7 +805,6 @@ class FileCompiler extends Wire {
}
}
*/
$classes = get_declared_classes();
$classes = array_merge($classes, get_declared_interfaces());
@@ -703,6 +830,9 @@ class FileCompiler extends Wire {
$classes = array_merge($classes, $files);
if(!__NAMESPACE__) $classes = array_merge($classes, array_keys($this->wire('modules')->getInstallable()));
$rawPHP = $this->rawPHP;
$rawDequotedPHP = $this->rawDequotedPHP;
// update classes and interfaces
foreach($classes as $class) {
@@ -714,7 +844,7 @@ class FileCompiler extends Wire {
$ns = '';
}
if($ns) {}
if(stripos($data, $class) === false) continue; // quick exit if class name not referenced in data
if(stripos($rawDequotedPHP, $class) === false) continue; // quick exit if class name not referenced in data
$patterns = array(
// 1=open 2=close
@@ -727,26 +857,28 @@ class FileCompiler extends Wire {
"instanceof" => '(\sinstanceof\s+)' . $class . '([^_a-zA-Z0-9]|$)', // 'instanceof Page'
"$class " => '(\(\s*|,\s*)' . $class . '(\s+\$)', // type hinted '(Page $something' or '($foo, Page $something'
);
foreach($patterns as $check => $regex) {
if(stripos($data, $check) === false) continue;
if(!preg_match_all('/' . $regex . '/im', $data, $matches)) continue;
//echo "<pre>" . print_r($matches, true) . "</pre>";
if(stripos($rawDequotedPHP, $check) === false) continue;
if(!preg_match_all('/' . $regex . '/im', $rawDequotedPHP, $matches)) continue;
foreach($matches[0] as $key => $fullMatch) {
$open = $matches[1][$key];
$close = $matches[2][$key];
if(substr($open, -1) == '\\') continue; // if last character in open is '\' then skip the replacement
$className = __NAMESPACE__ ? '\\' . __NAMESPACE__ . '\\' . $class : '\\' . $class;
$data = str_replace($fullMatch, $open . $className . $close, $data);
$repl = $open . $className . $close;
$data = str_replace($fullMatch, $repl, $data);
$rawPHP = str_replace($fullMatch, $repl, $rawPHP);
$rawDequotedPHP = str_replace($fullMatch, $repl, $rawDequotedPHP);
}
}
}
// update PW procedural function calls
$functions = get_defined_functions();
$hasFunctionExists = strpos($data, 'function_exists') !== false;
$hasFunctionExists = strpos($rawDequotedPHP, 'function_exists') !== false;
foreach($functions['user'] as $function) {
@@ -761,14 +893,16 @@ class FileCompiler extends Wire {
}
if($ns) {}
/** @noinspection PhpUnusedLocalVariableInspection */
if(stripos($data, $function) === false) continue; // if function name not mentioned in data, quick exit
if(stripos($rawDequotedPHP, $function) === false) continue; // if function name not mentioned in data, quick exit
$n = 0;
while(preg_match_all('/^(.*?[()!;,@\[=\s.])' . $function . '\s*\(/im', $data, $matches)) {
while(preg_match_all('/^(.*?[()!;,@\[=\s.])' . $function . '\s*\(/im', $rawPHP, $matches)) {
foreach($matches[0] as $key => $fullMatch) {
$open = $matches[1][$key];
if(strpos($open, 'function') !== false) continue; // skip function defined with same name
$data = str_replace($fullMatch, $open . $functionName . '(', $data);
$repl = $open . $functionName . '(';
$data = str_replace($fullMatch, $repl, $data);
$rawPHP = str_replace($fullMatch, $repl, $rawPHP);
}
if(++$n > 5) break;
}
@@ -782,10 +916,10 @@ class FileCompiler extends Wire {
// update other function calls
$ns = __NAMESPACE__ ? "\\ProcessWire" : "";
if(strpos($data, 'class_parents(') !== false) {
if(strpos($rawDequotedPHP, 'class_parents(') !== false) {
$data = preg_replace('/\bclass_parents\(/', $ns . '\\wireClassParents(', $data);
}
if(strpos($data, 'class_implements(') !== false) {
if(strpos($rawDequotedPHP, 'class_implements(') !== false) {
$data = preg_replace('/\bclass_implements\(/', $ns . '\\wireClassImplements(', $data);
}

View File

@@ -77,6 +77,7 @@ endif;
* - Specify false to exclude all empty values (this is the default if not specified).
* - Specify true to allow all empty values to be retained.
* - Specify an array of keys (from data) that should be retained if you want some retained and not others.
* - Specify array of literal empty value types to retain, i.e. [ 0, '0', array(), false, null ].
* - Specify the digit 0 to retain values that are 0, but not other types of empty values.
* @param bool $beautify Beautify the encoded data when possible for better human readability? (requires PHP 5.4+)
* @return string String of JSON data
@@ -167,7 +168,7 @@ function wireRmdir($path, $recursive = false) {
* @param string $path May be a directory or a filename
* @param bool $recursive If set to true, all files and directories in $path will be recursively set as well.
* @param string $chmod If you want to set the mode to something other than PW's chmodFile/chmodDir settings,
you may override it by specifying it here. Ignored otherwise. Format should be a string, like "0755".
* you may override it by specifying it here. Ignored otherwise. Format should be a string, like "0755".
* @return bool Returns true if all changes were successful, or false if at least one chmod failed.
* @throws WireException when it receives incorrect chmod format
*
@@ -500,7 +501,7 @@ function wireRenderFile($filename, array $vars = array(), array $options = array
* - It will assume a ".php" extension if filename has no extension.
*
* Note this function produced direct output. To retrieve output as a return value, use the
* wireTemplateFile function instead.
* `wireRenderFile()` function instead.
*
* @param $filename
* @param array $vars Optional variables you want to hand to the include (associative array)
@@ -626,14 +627,96 @@ function wireIconMarkupFile($filename, $class = '') {
/**
* Given a quantity of bytes, return a more readable size string
*
* @param int $size
* @param int $bytes Quantity in bytes
* @param bool|int|array $small Make returned string as small as possible (default=false),
* …or specify integer 1 for $small option but with space between number and unit label.
* …or optionally specify $options argument here.
* @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).
* - `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.
* @return string
*
*/
function wireBytesStr($size) {
if($size < 1024) return number_format($size) . ' ' . __('bytes', __FILE__);
$kb = round($size / 1024);
return number_format($kb) . " " . __('kB', __FILE__); // kilobytes
function wireBytesStr($bytes, $small = false, $options = array()) {
$defaults = array(
'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();
// determine size value and units label
if($bytes < 1024) {
$val = $bytes;
if($small) {
$label = $val > 0 ? __('B', __FILE__) : ''; // bytes
} else {
$label = __('bytes', __FILE__);
}
} else if($bytes < 1000000) {
$val = $bytes / 1024;
$label = __('kB', __FILE__); // kilobytes
} else if($bytes < 1073741824) {
$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'] = '.';
}
}
// 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'];
}
}
// 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;
}
/**
@@ -757,6 +840,49 @@ function wireClassParents($className, $autoload = true) {
return $a;
}
/**
* Does given instance (or class) represent an instance of the given className (or class names)?
*
* @param object|string $instance Object instance to test (or string of its class name).
* @param string|array $className Class name or array of class names to test against.
* @param bool $autoload
* @return bool|string Returns one of the following:
* - boolean false if not an instance (whether $className argument is string or array).
* - boolean true if given a single $className (string) and $instance is an instance of it.
* - string of first matching class name if $className was an array of classes to test.
*
*/
function wireInstanceOf($instance, $className, $autoload = true) {
if(is_array($className)) {
$returnClass = true;
$classNames = $className;
} else {
$returnClass = false;
$classNames = array($className);
}
$matchClass = null;
$instanceParents = null;
foreach($classNames as $className) {
$className = wireClassName($className, true); // with namespace
if(is_object($instance) && class_exists($className, $autoload)) {
if($instance instanceof $className) $matchClass = $className;
} else {
if(is_null($instanceParents)) {
$instanceParents = wireClassParents($instance, $autoload);
$instanceClass = is_string($instance) ? $instance : wireClassName($instance, true);
$instanceParents[$instanceClass] = 1;
}
if(isset($parents[$className])) $matchClass = $className;
}
if($matchClass !== null) break;
}
return $returnClass ? $matchClass : ($matchClass !== null);
}
/**
* ProcessWire namespace aware version of PHP's is_callable() function
*
@@ -771,6 +897,25 @@ function wireIsCallable($var, $syntaxOnly = false, &$callableName = '') {
return is_callable($var, $syntaxOnly, $callableName);
}
/**
* Return the count of item(s) present in the given value
*
* Duplicates behavior of PHP count() function prior to PHP 7.2, which states:
* Returns the number of elements in $value. When the parameter is neither an array nor an
* object with implemented Countable interface, 1 will be returned. There is one exception,
* if $value is NULL, 0 will be returned.
*
* @param mixed $value
* @return int
*
*/
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);
return 1;
}
/**
* Get or set an output region (primarily for front-end output usage)
*
@@ -801,13 +946,16 @@ function wireIsCallable($var, $syntaxOnly = false, &$callableName = '') {
* - Specify "*" to retrieve all defined regions in an array.
* - Prepend a "+" to the region name to have it prepend your given value to any existing value.
* - Append a "+" to the region name to have it append your given value to any existing value.
* - Prepend a "++" to region name to make future calls without "+" automatically prepend.
* - Append a "++" to region name to make future calls without "+" to automatically append.
* @param null|string $value If setting a region, the text that you want to set.
* @return string|null|bool|array Returns string of text when getting a region, NULL if region not set, or TRUE if setting region.
*
*/
function wireRegion($key, $value = null) {
static $regions = array();
static $locked = array();
if(empty($key) || $key === '*') {
// all regions
@@ -822,17 +970,24 @@ function wireRegion($key, $value = null) {
} else {
// set region
$pos = strpos($key, '+');
if($pos !== false) $key = trim($key, '+');
if($pos !== false) {
$lock = strpos($key, '++') !== false;
$key = trim($key, '+');
if($lock !== false && !isset($locked[$key])) {
$locked[$key] = $lock === 0 ? '^' : '$'; // prepend : append
}
}
$lock = isset($locked[$key]) ? $locked[$key] : '';
if(!isset($regions[$key])) $regions[$key] = '';
if($pos === 0) {
if($pos === 0 || ($pos === false && $lock == '^')) {
// prepend
$regions[$key] = $value . $regions[$key];
} else if($pos) {
} else if($pos || ($pos === false && $lock == '$')) {
// append
$regions[$key] .= $value;
} else if($value === '') {
// clear region
unset($regions[$key]);
if(!$lock) unset($regions[$key]);
} else {
// insert/replace
$regions[$key] = $value;

View File

@@ -378,6 +378,19 @@ function paths($key = '') {
return wirePaths($key);
}
/**
* Start or stop a profiler event or return WireProfilerInterface instance
*
* @param string|array|object|null $name Name of event to start or event to stop
* @param null|object|string $source If starting an event, optional source of event (object)
* @param array $data Optional extra data as associative array
* @return null|array|object
*
*/
function profiler($name = null, $source = null, $data = array()) {
return wireProfiler($name, $source, $data);
}
/**
* Get or set a region for front-end output
*

View File

@@ -460,6 +460,26 @@ function wireLog($logName = '', $message = '') {
return $log;
}
/**
* Start or stop a profiler event or return WireProfilerInterface instance
*
* @param string|array|object|null $name Name of event to start or event to stop
* @param null|object|string $source If starting an event, optional source of event (object)
* @param array $data Optional extra data as associative array
* @return null|array|object
*
*/
function wireProfiler($name = null, $source = null, $data = array()) {
$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);
}
}
/**
* Function that returns a $config->urls->[name] value o
*

View File

@@ -9,6 +9,7 @@
* Instances of HookEvent are passed to Hook handlers when their requested method has been called.
*
* #pw-summary HookEvent is a type provided to hook functions with information about the event.
* #pw-var $event
* #pw-body =
* ~~~~~~
* // Example
@@ -23,7 +24,7 @@
*
* @property-read Wire|WireData|WireArray|Module $object Instance of the object where the Hook event originated.
* @property-read string $method The name of the method that was called to generate the Hook event.
* @property-read array $arguments A numerically indexed array of the arguments sent to the above mentioned method.
* @property array $arguments A numerically indexed array of the arguments sent to the above mentioned method.
* @property mixed $return Applicable only for 'after' or ('replace' + 'before' hooks), contains the value returned by the above mentioned method. The hook handling method may modify this return value.
* @property bool $replace Set to boolean true in a 'before' hook if you want to prevent execution of the original hooked function. In such a case, your hook is replacing the function entirely. Not recommended, so be careful with this.
* @property array $options An optional array of user-specified data that gets sent to the hooked function. The hook handling method may access it from $event->data. Also includes all the default hook properties.
@@ -42,17 +43,23 @@ class HookEvent extends WireData {
/**
* Construct the HookEvent and establish default values
*
* @param array $eventData Optional event data to start with
*
*/
public function __construct() {
$this->set('object', null);
$this->set('method', '');
$this->set('arguments', array());
$this->set('return', null);
$this->set('replace', false);
$this->set('options', array());
$this->set('id', '');
$this->set('cancelHooks', false);
public function __construct(array $eventData = array()) {
$data = array(
'object' => null,
'method' => '',
'arguments' => array(),
'return' => null,
'replace' => false,
'options' => array(),
'id' => '',
'cancelHooks' => false
);
if(!empty($eventData)) $data = array_merge($data, $eventData);
$this->data = $data;
}
/**
@@ -201,7 +208,7 @@ class HookEvent extends WireData {
* ~~~~~
*
* @param string|null $hookId
* @return $this
* @return HookEvent|WireData $this
*
*/
public function removeHook($hookId) {
@@ -226,6 +233,5 @@ class HookEvent extends WireData {
return $s;
}
}

View File

@@ -232,7 +232,7 @@ class ImageInspector extends WireData {
$i['trans'] = isset($gi->m_bTrans) ? $gi->m_bTrans : false;
$i['transcolor'] = isset($gi->m_nTrans) ? $gi->m_nTrans : '';
$i['bgcolor'] = $gfh->m_nBgColor;
$i['numcolors'] = $gfh->m_colorTable->m_nColors;
$i['numcolors'] = isset($gfh->m_colorTable->m_nColors) ? $gfh->m_colorTable->m_nColors : 0;
$i['interlace'] = $gih->m_bInterlace;
$this->info = $i;
unset($gif, $gih, $gfh, $gi, $i);

View File

@@ -88,20 +88,8 @@ class ImageSizer extends Wire {
*
*/
public function __construct($filename = '', $options = array()) {
if(isset($options['forceEngine'])) {
$this->forceEngineName = $options['forceEngine'];
unset($options['forceEngine']);
}
$this->filename = $filename;
$this->initialOptions = $options;
if(strlen($filename)) {
$imageInspector = new ImageInspector($filename);
$this->inspectionResult = $imageInspector->inspect($filename, true);
$this->engine = $this->newImageSizerEngine($filename, $options, $this->inspectionResult);
}
if(!empty($options)) $this->setOptions($options);
if(!empty($filename)) $this->setFilename($filename);
}
/**
@@ -158,6 +146,7 @@ class ImageSizer extends Wire {
if(empty($inspectionResult) && $filename && is_readable($filename)) {
$imageInspector = new ImageInspector($filename);
$this->wire($imageInspector);
$inspectionResult = $imageInspector->inspect($filename, true);
$this->inspectionResult = $inspectionResult;
}
@@ -226,17 +215,8 @@ class ImageSizer extends Wire {
*/
public function ___resize($targetWidth, $targetHeight = 0) {
if(empty($this->filename)) throw new WireException('No file to resize: please call setFilename($file) before resize()');
if(empty($this->engine)) {
// set the engine, and check if the engine is ready to use
$this->engine = $this->newImageSizerEngine();
if(!$this->engine) {
throw new WireException('There seems to be no support for the GD image library on your host?');
}
}
$success = $this->engine->resize($targetWidth, $targetHeight);
$engine = $this->getEngine();
$success = $engine->resize($targetWidth, $targetHeight);
if(!$success) {
// fallback to GD
@@ -297,8 +277,12 @@ class ImageSizer extends Wire {
*
*/
public function setOptions(array $options) {
if(isset($options['forceEngine'])) {
$this->setForceEngine($options['forceEngine']);
unset($options['forceEngine']);
}
$this->initialOptions = array_merge($this->initialOptions, $options);
if($this->engine) $this->engine->setOptions($options);
if($this->engine) $this->engine->setOptions($this->initialOptions);
return $this;
}
@@ -329,16 +313,48 @@ class ImageSizer extends Wire {
public function setUpscaling($value = true) { return $this->setOptions(array('upscaling', $value)); }
public function setUseUSM($value = true) { return $this->setOptions(array('useUSM', $value)); }
// getters (@todo phpdocs)
public function getWidth() { return $this->engine->image['width']; }
public function getHeight() { return $this->engine->image['height']; }
public function getFilename() { return $this->engine->filename; }
public function getExtension() { return $this->engine->extension; }
public function getImageType() { return $this->engine->imageType; }
public function isModified() { return $this->engine->modified; }
public function getOptions() { return $this->engine->getOptions(); }
public function getEngine() { return $this->engine; }
public function __get($key) { return $this->engine->__get($key); }
public function getWidth() {
$image = $this->getEngine()->get('image');
return $image['width'];
}
public function getHeight() {
$image = $this->getEngine()->get('image');
return $image['height'];
}
public function getFilename() { return $this->getEngine()->filename; }
public function getExtension() { return $this->getEngine()->extension; }
public function getImageType() { return $this->getEngine()->imageType; }
public function isModified() { return $this->getEngine()->modified; }
public function getOptions() { return $this->getEngine()->getOptions(); }
/**
* Get the current ImageSizerEngine
*
* @return ImageSizerEngine
* @throws WireException
*
*/
public function getEngine() {
if($this->engine) return $this->engine;
if(empty($this->filename)) {
throw new WireException('No file to process: please call setFilename($file) before calling other methods');
}
$imageInspector = new ImageInspector($this->filename);
$this->inspectionResult = $imageInspector->inspect($this->filename, true);
$this->engine = $this->newImageSizerEngine($this->filename, $this->initialOptions, $this->inspectionResult);
// set the engine, and check if the engine is ready to use
if(!$this->engine) {
throw new WireException('There seems to be no support for the GD image library on your host?');
}
return $this->engine;
}
public function __get($key) { return $this->getEngine()->__get($key); }
/**
* ImageInformation from Image Inspector in short form or full RawInfoData
@@ -348,7 +364,8 @@ class ImageSizer extends Wire {
*
*/
public function getImageInfo($rawData = false) {
$this->getEngine();
if($rawData) return $this->inspectionResult;
$imageType = $this->inspectionResult['info']['imageType'];
$type = '';
@@ -507,7 +524,9 @@ class ImageSizer extends Wire {
*
*/
static public function imageResetIPTC($image) {
$wire = null;
if($image instanceof Pageimage) {
$wire = $image;
$filename = $image->filename;
} else if(is_readable($image)) {
$filename = $image;
@@ -515,8 +534,72 @@ class ImageSizer extends Wire {
return null;
}
$sizer = new ImageSizerEngineGD($filename);
if($wire) $wire->wire($sizer);
$result = false !== $sizer->writeBackIPTC($filename) ? true : false;
return $result;
}
/**
* Rotate image by given degrees
*
* @param int $degrees
* @return bool
*
*/
public function rotate($degrees) {
return $this->getEngine()->rotate($degrees);
}
/**
* Flip image vertically
*
* @return bool
*
*/
public function flipVertical() {
return $this->getEngine()->flipVertical();
}
/**
* Flip image horizontally
*
* @return bool
*
*/
public function flipHorizontal() {
return $this->getEngine()->flipHorizontal();
}
/**
* Flip both vertically and horizontally
*
* @return bool
*
*/
public function flipBoth() {
return $this->getEngine()->flipBoth();
}
/**
* Convert image to greyscale (black and white)
*
* @return bool
*
*/
public function convertToGreyscale() {
return $this->getEngine()->convertToGreyscale();
}
/**
* Convert image to sepia tone
*
* @param int $sepia Sepia amount
* @return bool
*
*/
public function convertToSepia($sepia = 55) {
return $this->getEngine()->convertToSepia('', $sepia);
}
}

View File

@@ -8,6 +8,7 @@
*
* @property bool $autoRotation
* @property bool $upscaling
* @property bool $interlace
* @property array|string|bool $cropping
* @property int $quality
* @property string $sharpening
@@ -59,6 +60,14 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*/
protected $quality = 90;
/**
* Image interlace setting, false or true
*
* @var bool
*
*/
protected $interlace = false;
/**
* Information about the image (width/height)
*
@@ -100,9 +109,11 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*
* Possible values: northwest, north, northeast, west, center, east, southwest, south, southeast
* or TRUE to crop to center, or FALSE to disable cropping.
* Or array where index 0 is % or px from left, and index 1 is % or px from top. Percent is assumed if
* values are number strings that end with %. Pixels are assumed of values are just integers.
* Default is: TRUE
*
* @var bool
* @var bool|array
*
*/
protected $cropping = true;
@@ -206,6 +217,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
'autoRotation',
'upscaling',
'cropping',
'interlace',
'quality',
'sharpening',
'defaultGamma',
@@ -388,6 +400,36 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*/
abstract protected function processResize($srcFilename, $dstFilename, $fullWidth, $fullHeight, $finalWidth, $finalHeight);
/**
* Process rotate of an image
*
* @param string $srcFilename
* @param string $dstFilename
* @param int $degrees Clockwise degrees, i.e. 90, 180, 270, -90, -180, -270
* @return bool
*
*/
protected function processRotate($srcFilename, $dstFilename, $degrees) {
if($srcFilename && $dstFilename && $degrees) {}
$this->error('rotate not implemented for ' . $this->className());
return false;
}
/**
* Process vertical or horizontal flip of an image
*
* @param string $srcFilename
* @param string $dstFilename
* @param bool $flipVertical True if flip is vertical, false if flip is horizontal
* @return bool
*
*/
protected function processFlip($srcFilename, $dstFilename, $flipVertical) {
if($srcFilename && $dstFilename && $flipVertical) {}
$this->error('flip not implemented for ' . $this->className());
return false;
}
/**
* Get array of image file extensions this ImageSizerModule can process
*
@@ -623,6 +665,10 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
$pWidth = $this->getProportionalWidth($targetHeight);
}
// rounding issue fix via @horst-n for #191
if($targetWidth == $originalTargetWidth && 1 + $targetWidth == $pWidth) $pWidth = $pWidth - 1;
if($targetHeight == $originalTargetHeight && 1 + $targetHeight == $pHeight) $pHeight = $pHeight - 1;
if(!$this->upscaling) {
// we are going to shoot for something smaller than the target
@@ -699,10 +745,29 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
$cropping = strtolower($cropping);
if(strpos($cropping, ',')) {
$cropping = explode(',', $cropping);
if(strpos($cropping[0], '%') !== false) $cropping[0] = round(min(100, max(0, $cropping[0]))) . '%';
else $cropping[0] = (int) $cropping[0];
if(strpos($cropping[1], '%') !== false) $cropping[1] = round(min(100, max(0, $cropping[1]))) . '%';
else $cropping[1] = (int) $cropping[1];
} else if(strpos($cropping, 'x') && preg_match('/^([pd])(\d+)x(\d+)(z\d+)?/', $cropping, $matches)) {
$cropping = array(0 => $matches[1], 1 => $matches[2]);
if(isset($matches[3])) $cropping[2] = (int) $matches[3];
if($matches[1] == 'p') {
$cropping[0] .= '%';
$cropping[0] .= '%';
}
}
}
if(is_array($cropping)) {
if(strpos($cropping[0], '%') !== false) {
$cropping[0] = round(min(100, max(0, $cropping[0]))) . '%';
} else {
$cropping[0] = (int) $cropping[0];
}
if(strpos($cropping[1], '%') !== false) {
$cropping[1] = round(min(100, max(0, $cropping[1]))) . '%';
} else {
$cropping[1] = (int) $cropping[1];
}
if(isset($cropping[2])) { // zoom
$cropping[2] = (int) $cropping[2];
if($cropping[2] < 2 || $cropping[2] > 99) unset($cropping[2]);
}
}
@@ -732,8 +797,12 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
// crop name if custom center point is specified
if(is_array($cropping)) {
// p = percent, d = pixel dimension
$cropping = (strpos($cropping[0], '%') !== false ? 'p' : 'd') . ((int) $cropping[0]) . 'x' . ((int) $cropping[1]);
// p = percent, d = pixel dimension, z = zoom
$zoom = isset($cropping[2]) ? (int) $cropping[2] : 0;
$cropping =
(strpos($cropping[0], '%') !== false ? 'p' : 'd') .
((int) $cropping[0]) . 'x' . ((int) $cropping[1]);
if($zoom > 1 && $zoom < 100) $cropping .= "z$zoom";
}
// if crop is TRUE or FALSE, we don't reflect that in the filename, so make it blank
@@ -790,11 +859,13 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
if(isset($value[$v])) $$v = $value[$v];
}
}
foreach(array('x', 'y', 'w', 'h') as $k) {
$v = isset($$k) ? $$k : -1;
if(!is_int($v) || $v < 0) throw new WireException("Missing or wrong param $k for ImageSizer-cropExtra!");
if(('w' == $k || 'h' == $k) && 0 == $v) throw new WireException("Wrong param $k for ImageSizer-cropExtra!");
$v = (int) (isset($$k) ? $$k : -1);
if(!$v && $k == 'w' && $h > 0) $v = $this->getProportionalWidth((int) $h);
if(!$v && $k == 'h' && $w > 0) $v = $this->getProportionalHeight((int) $w);
if($v < 0) throw new WireException("Missing or wrong param $k=$v for ImageSizer-cropExtra! " . print_r($value, true));
if(('w' == $k || 'h' == $k) && 0 == $v) throw new WireException("Wrong param $k=$v for ImageSizer-cropExtra! " . print_r($value, true));
}
$this->cropExtra = array($x, $y, $w, $h);
@@ -906,6 +977,19 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
$this->upscaling = $this->getBooleanValue($value);
return $this;
}
/**
* Turn on/off interlace
*
* @param bool $value Whether to upscale or not (default = true)
*
* @return $this
*
*/
public function setInterlace($value = true) {
$this->interlace = $this->getBooleanValue($value);
return $this;
}
/**
* Set default gamma value: 0.5 - 4.0 | -1
@@ -1055,6 +1139,9 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
case 'upscaling':
$this->setUpscaling($value);
break;
case 'interlace':
$this->setInterlace($value);
break;
case 'sharpening':
$this->setSharpening($value);
break;
@@ -1083,7 +1170,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
$this->setFlip($value);
break;
case 'useUSM':
$this->setUseUsm($value);
$this->setUseUSM($value);
break;
default:
@@ -1122,6 +1209,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
'quality' => $this->quality,
'cropping' => $this->cropping,
'upscaling' => $this->upscaling,
'interlace' => $this->interlace,
'autoRotation' => $this->autoRotation,
'sharpening' => $this->sharpening,
'defaultGamma' => $this->defaultGamma,
@@ -1323,7 +1411,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
protected function getCropDimensions(&$w1, &$h1, $gdWidth, $targetWidth, $gdHeight, $targetHeight) {
if(is_string($this->cropping)) {
// calculate from 8 named cropping points
switch($this->cropping) {
case 'nw':
$w1 = 0;
@@ -1357,20 +1445,45 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
}
} else if(is_array($this->cropping)) {
// calculate from specific percent or pixels from left and top
// $this->cropping is an array with the following:
// index 0 represents % or pixels from left
// index 1 represents % or pixels from top
// @interrobang + @u-nikos
if(strpos($this->cropping[0], '%') === false) $pointX = (int) $this->cropping[0];
else $pointX = $gdWidth * ((int) $this->cropping[0] / 100);
if(strpos($this->cropping[0], '%') === false) {
$pointX = (int) $this->cropping[0];
} else {
$pointX = $gdWidth * ((int) $this->cropping[0] / 100);
}
if(strpos($this->cropping[1], '%') === false) $pointY = (int) $this->cropping[1];
else $pointY = $gdHeight * ((int) $this->cropping[1] / 100);
if(strpos($this->cropping[1], '%') === false) {
$pointY = (int) $this->cropping[1];
} else {
$pointY = $gdHeight * ((int) $this->cropping[1] / 100);
}
/*
if(isset($this->cropping[2]) && $this->cropping[2] > 1) {
// zoom percent (2-100)
$zoom = (int) $this->cropping[2];
}
*/
if($pointX < $targetWidth / 2) $w1 = 0;
else if($pointX > ($gdWidth - $targetWidth / 2)) $w1 = $gdWidth - $targetWidth;
else $w1 = $pointX - $targetWidth / 2;
if($pointX < $targetWidth / 2) {
$w1 = 0;
} else if($pointX > ($gdWidth - $targetWidth / 2)) {
$w1 = $gdWidth - $targetWidth;
} else {
$w1 = $pointX - $targetWidth / 2;
}
if($pointY < $targetHeight / 2) $h1 = 0;
else if($pointY > ($gdHeight - $targetHeight / 2)) $h1 = $gdHeight - $targetHeight;
else $h1 = $pointY - $targetHeight / 2;
if($pointY < $targetHeight / 2) {
$h1 = 0;
} else if($pointY > ($gdHeight - $targetHeight / 2)) {
$h1 = $gdHeight - $targetHeight;
} else {
$h1 = $pointY - $targetHeight / 2;
}
}
}
@@ -1406,8 +1519,6 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
$this->fullHeight = $this->image['height'];
if(0 == $this->finalWidth && 0 == $this->finalHeight) return false;
if(0 == $this->finalWidth) $this->finalWidth = ceil(($this->finalHeight / $this->fullHeight) * $this->fullWidth);
if(0 == $this->finalHeight) $this->finalHeight = ceil(($this->finalWidth / $this->fullWidth) * $this->fullHeight);
if($this->scale !== 1.0) { // adjust for hidpi
if($this->finalWidth) $this->finalWidth = ceil($this->finalWidth * $this->scale);
@@ -1440,6 +1551,118 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
return true;
}
/**
* Just rotate image by number of degrees
*
* @param int $degrees
* @param string $dstFilename Optional destination filename. If not present, source will be overwritten.
* @return bool True on success, false on fail
*
*/
public function rotate($degrees, $dstFilename = '') {
$degrees = (int) $degrees;
$srcFilename = $this->filename;
if(empty($dstFilename)) $dstFilename = $srcFilename;
if($degrees > 360) $degrees = 360 - $degrees;
if($degrees < -360) $degrees = $degrees - 360;
if($degrees == 0 || $degrees == 360 || $degrees == -360) {
if($dstFilename != $this->filename) wireCopy($this->filename, $dstFilename);
return true;
}
if($srcFilename == $dstFilename) {
// src and dest are the same, so use a temporary file
$n = 1;
do {
$tmpFilename = dirname($dstFilename) . "/.ise$n-" . basename($dstFilename);
} while(file_exists($tmpFilename) && $n++);
} else {
// src and dest are different files
$tmpFilename = $dstFilename;
}
$result = $this->processRotate($srcFilename, $tmpFilename, $degrees);
if($result) {
// success
if($tmpFilename != $dstFilename) {
if(is_file($dstFilename)) unlink($dstFilename);
rename($tmpFilename, $dstFilename);
}
wireChmod($dstFilename);
} else {
// fail
if(is_file($tmpFilename)) unlink($tmpFilename);
}
return $result;
}
/**
* Flip vertically
*
* @param string $dstFilename
* @return bool
*
*/
public function flipVertical($dstFilename = '') {
if(empty($dstFilename)) $dstFilename = $this->filename;
return $this->processFlip($this->filename, $dstFilename, 'vertical');
}
/**
* Flip horizontally
*
* @param string $dstFilename
* @return bool
*
*/
public function flipHorizontal($dstFilename = '') {
if(empty($dstFilename)) $dstFilename = $this->filename;
return $this->processFlip($this->filename, $dstFilename, 'horizontal');
}
/**
* Flip both vertically and horizontally
*
* @param string $dstFilename
* @return bool
*
*/
public function flipBoth($dstFilename = '') {
if(empty($dstFilename)) $dstFilename = $this->filename;
return $this->processFlip($this->filename, $dstFilename, 'both');
}
/**
* Convert image to greyscale
*
* @param string $dstFilename If different from source file
* @return bool
*
*/
public function convertToGreyscale($dstFilename = '') {
if($dstFilename) {}
return false;
}
/**
* Convert image to sepia
*
* @param string $dstFilename If different from source file
* @param float|int $sepia Sepia value
* @return bool
*
*/
public function convertToSepia($dstFilename = '', $sepia = 55) {
if($dstFilename && $sepia) {}
return false;
}
/**
* Get an integer representing the resize method to use
*
@@ -1473,6 +1696,121 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
return 4;
}
/**
* Helper function to perform a cropExtra / cropBefore cropping
*
* Intended for use by the getFocusZoomCropDimensions() method
*
* @param string $focus (focus point in percent, like: 54.7%)
* @param int $sourceDimension (source image width or height)
* @param int $cropDimension (target crop-image width or height)
* @param int $zoom
*
* @return int $position (crop position x or y in pixel)
*
*/
protected function getFocusZoomPosition($focus, $sourceDimension, $cropDimension, $zoom) {
$focus = intval($focus); // string with float value and percent char, (needs to be converted to integer)
$scale = 1 + (($zoom / 100) * 2);
$focusPX = ($sourceDimension / 100 * $focus);
$posMinPX = $cropDimension / 2 / $scale;
$posMaxPX = $sourceDimension - ($cropDimension / 2);
// calculate the position in pixel !
if($focusPX >= $posMaxPX) {
$posPX = $sourceDimension - $cropDimension;
} else if($focusPX <= $posMinPX) {
$posPX = 0;
} else {
$posPX = $focusPX - ($cropDimension / 2);
if(0 > $posPX) $posPX = 0;
}
return $posPX;
}
/**
* Get an array of the 4 dimensions necessary to perform a cropExtra / cropBefore cropping
*
* Intended for use by the resize() method
*
* @param int $zoom
* @param int $fullWidth
* @param int $fullHeight
* @param int $finalWidth
* @param int $finalHeight
* @return array
*
*/
protected function getFocusZoomCropDimensions($zoom, $fullWidth, $fullHeight, $finalWidth, $finalHeight) {
// validate & calculate / prepare params
$zoom = $zoom <= 70 ? $zoom : 70; // validate / correct the zoom value, it needs to be between 2 and 70
$zoom = $zoom >= 2 ? $zoom : 2;
// calculate the max crop dimensions
$ratioFinal = $finalWidth / $finalHeight; // get the ratio of the requested crop
$percentW = $finalWidth / $fullWidth * 100; // calculate percentage of the crop width in regard of the original width
$percentH = $finalHeight / $fullHeight * 100; // calculate percentage of the crop height in regard of the original height
if($percentW >= $percentH) { // check wich one is greater
$maxW = $fullWidth; // if percentW is greater, maxW becomes the original Width
$maxH = $fullWidth / $ratioFinal; // ... and maxH gets calculated via the ratio
} else {
$maxH = $fullHeight; // if percentH is greater, maxH becomes the original Height
$maxW = $fullHeight * $ratioFinal; // ... and maxW gets calculated via the ratio
}
// calculate the zoomed dimensions
$cropW = $maxW - ($maxW * $zoom / 100); // to get the final crop Width and Height, the amount for zoom-in
$cropH = $maxH - ($maxH * $zoom / 100); // needs to get stripped out
// validate against the minimal dimensions
if(!$this->upscaling) { // if upscaling isn't allowed, we decrease the zoom, so that we get a crop with the min-Dimensions
if($cropW < $finalWidth) {
$cropW = $finalWidth;
$cropH = $finalWidth / $ratioFinal;
}
if($cropH < $finalHeight) {
$cropH = $finalHeight;
$cropW = $finalHeight * $ratioFinal;
}
}
// calculate the crop positions
$posX = $this->getFocusZoomPosition($this->cropping[0], $fullWidth, $cropW, $zoom); // calculate the x-position
$posY = $this->getFocusZoomPosition($this->cropping[1], $fullHeight, $cropH, $zoom); // calculate the y-position
return array(
0 => (int) $posX,
1 => (int) $posY,
2 => (int) $cropW,
3 => (int) $cropH
);
}
/**
* Get current zoom percentage setting or 0 if not set
*
* Value is determined from the $this->cropping array index 2 and is used only if index 0 and
* index 1 are percentages (and indicated as such with a percent sign).
*
* @return int
*
*/
protected function getFocusZoomPercent() {
// check if we have to proceed a zoomed focal point cropping,
// therefore we need index 0 and 1 to be strings with '%' sign included
// and index 2 to be an integer between 2 and 70
$a = $this->cropping;
if(is_array($a) && isset($a[2]) && strpos($a[0], '%') !== false && strpos($a[1], '%') !== false) {
$zoom = (int) $a[2];
if($zoom < 2) $zoom = 0;
if($zoom > 70) $zoom = 70;
} else {
$zoom = 0;
}
return $zoom;
}
/**
* Module info: not-autoload
*

View File

@@ -102,11 +102,12 @@ class ImageSizerEngineGD extends ImageSizerEngine {
protected function processResize($srcFilename, $dstFilename, $fullWidth, $fullHeight, $finalWidth, $finalHeight) {
$this->modified = false;
$isModified = false;
if(isset($this->info['bits'])) $this->imageDepth = $this->info['bits'];
$this->imageFormat = strtoupper(str_replace('image/', '', $this->info['mime']));
if(!in_array($this->imageFormat, $this->validSourceImageFormats())) {
throw new WireException(sprintf($this->_("loaded file '%s' is not in the list of valid images", basename($dstFilename))));
throw new WireException(sprintf($this->_("loaded file '%s' is not in the list of valid images"), basename($dstFilename)));
}
$image = null;
@@ -141,6 +142,7 @@ class ImageSizerEngineGD extends ImageSizerEngine {
if($this->rotate || $needRotation) { // @horst
$degrees = $this->rotate ? $this->rotate : $orientations[0];
$image = $this->imRotate($image, $degrees);
$isModified = true;
if(abs($degrees) == 90 || abs($degrees) == 270) {
// we have to swap width & height now!
$tmp = array($this->getWidth(), $this->getHeight());
@@ -155,7 +157,17 @@ class ImageSizerEngineGD extends ImageSizerEngine {
} else if($orientations[1] > 0) {
$vertical = $orientations[1] == 2;
}
if(!is_null($vertical)) $image = $this->imFlip($image, $vertical);
if(!is_null($vertical)) {
$image = $this->imFlip($image, $vertical);
$isModified = true;
}
}
$zoom = $this->getFocusZoomPercent();
if($zoom > 1) {
// we need to configure a cropExtra call to respect the zoom factor
$this->cropExtra = $this->getFocusZoomCropDimensions($zoom, $fullWidth, $fullHeight, $finalWidth, $finalHeight);
$this->cropping = false;
}
// if there is requested to crop _before_ resize, we do it here @horst
@@ -182,9 +194,11 @@ class ImageSizerEngineGD extends ImageSizerEngine {
$this->prepareImageLayer($image, $imageTemp);
imagecopy($image, $imageTemp, 0, 0, $x, $y, $w, $h);
unset($x, $y, $w, $h);
$isModified = true;
// now release the intermediate image and update settings
imagedestroy($imageTemp);
$imageTemp = null;
$this->setImageInfo(imagesx($image), imagesy($image));
// $this->cropping = false; // ?? set this to prevent overhead with the following manipulation ??
}
@@ -202,12 +216,20 @@ class ImageSizerEngineGD extends ImageSizerEngine {
// this is the case if the original size is requested or a greater size but upscaling is set to false
// the current version is allready the desired result, we only may have to apply compression where possible
$this->sharpening = 'none'; // we set sharpening to none
// current version is already the desired result, we only may have to compress JPEGs but leave GIF and PNG as is:
if(!$isModified && ($this->imageType == \IMAGETYPE_PNG || $this->imageType == \IMAGETYPE_GIF)) {
$result = @copy($srcFilename, $dstFilename);
if(isset($image) && is_resource($image)) @imagedestroy($image); // clean up
if(isset($image)) $image = null;
return $result; // early return !
}
// process JPEGs
if(self::checkMemoryForImage(array(imagesx($image), imagesy($image), 3)) === false) {
throw new WireException(basename($srcFilename) . " - not enough memory to copy the final image");
}
$this->sharpening = 'none'; // we set sharpening to none, as the image only gets compressed, but not resized
$thumb = imagecreatetruecolor(imagesx($image), imagesy($image)); // create the final memory image
$this->prepareImageLayer($thumb, $image);
imagecopy($thumb, $image, 0, 0, 0, 0, imagesx($image), imagesy($image)); // copy our intermediate image into the final one
@@ -231,10 +253,40 @@ class ImageSizerEngineGD extends ImageSizerEngine {
if(self::checkMemoryForImage(array($bgWidth, $bgHeight, 3)) === false) {
throw new WireException(basename($srcFilename) . " - not enough memory to resize to the intermediate image");
}
$sourceX = 0;
$sourceY = 0;
$sourceWidth = $this->image['width'];
$sourceHeight = $this->image['height'];
/*
* @todo figure out how to make zoom setting adjust coordinates to imagecopyresampled() calls
$zoom = is_array($this->cropping) && isset($this->cropping[2]) ? $this->cropping[2] : 0;
if($zoom > 1) {
$zoom = $zoom * 0.01;
$sourceWidth -= $sourceWidth * $zoom;
$sourceHeight -= $sourceHeight * $zoom;
$sourceX = $this->image['width'] - ($sourceWidth / 2);
$sourceY = $this->image['height'] - ($sourceHeight / 2);
$bgX = 0;
$bgY = 0;
}
*/
$thumb2 = imagecreatetruecolor($bgWidth, $bgHeight);
$this->prepareImageLayer($thumb2, $image);
imagecopyresampled($thumb2, $image, 0, 0, 0, 0, $bgWidth, $bgHeight, $this->image['width'], $this->image['height']);
imagecopyresampled(
$thumb2, // destination image
$image, // source image
0, // destination X
0, // destination Y
$sourceX, // source X
$sourceY, // source Y
$bgWidth, // destination width
$bgHeight, // destination height
$sourceWidth, // source width
$sourceHeight // source height
);
if(self::checkMemoryForImage(array($finalWidth, $finalHeight, 3)) === false) {
throw new WireException(basename($srcFilename) . " - not enough memory to crop to the final image");
@@ -242,7 +294,18 @@ class ImageSizerEngineGD extends ImageSizerEngine {
$thumb = imagecreatetruecolor($finalWidth, $finalHeight);
$this->prepareImageLayer($thumb, $image);
imagecopyresampled($thumb, $thumb2, 0, 0, $bgX, $bgY, $finalWidth, $finalHeight, $finalWidth, $finalHeight);
imagecopyresampled(
$thumb, // destination image
$thumb2, // source image
0, // destination X
0, // destination Y
$bgX, // source X
$bgY, // source Y
$finalWidth, // destination width
$finalHeight, // destination height
$finalWidth, // source width
$finalHeight // source height
);
imagedestroy($thumb2);
}
@@ -273,6 +336,15 @@ class ImageSizerEngineGD extends ImageSizerEngine {
}
}
// optionally apply interlace bit to the final image.
// this will result in progressive JPEGs
if($this->interlace && \IMAGETYPE_JPEG == $this->imageType) {
if(0 == imageinterlace($thumb, 1)) {
// log that setting the interlace bit has failed ?
// ...
}
}
// write to file
$result = false;
switch($this->imageType) {
@@ -299,7 +371,6 @@ class ImageSizerEngineGD extends ImageSizerEngine {
return $result;
}
/**
* Rotate image (@horst)
*
@@ -313,7 +384,8 @@ class ImageSizerEngineGD extends ImageSizerEngine {
$degree = (is_float($degree) || is_int($degree)) && $degree > -361 && $degree < 361 ? $degree : false;
if($degree === false) return $im;
if(in_array($degree, array(-360, 0, 360))) return $im;
return @imagerotate($im, $degree, imagecolorallocate($im, 0, 0, 0));
$angle = 360 - $degree; // because imagerotate() expects counterclockwise angle rather than degrees
return @imagerotate($im, $angle, imagecolorallocate($im, 0, 0, 0));
}
/**
@@ -397,7 +469,7 @@ class ImageSizerEngineGD extends ImageSizerEngine {
$amount = intval($amount / 100 * $this->usmValue);
// apply unsharp mask filter
return $this->UnsharpMask($im, $amount, $radius, $threshold);
return $this->unsharpMask($im, $amount, $radius, $threshold);
}
// if we do not use USM, we use our default sharpening method,
@@ -541,12 +613,12 @@ class ImageSizerEngineGD extends ImageSizerEngine {
for($x = 0; $x < $w - 1; $x++) { // each row
for($y = 0; $y < $h; $y++) { // each pixel
$rgbOrig = ImageColorAt($img, $x, $y);
$rgbOrig = imagecolorat($img, $x, $y);
$rOrig = (($rgbOrig >> 16) & 0xFF);
$gOrig = (($rgbOrig >> 8) & 0xFF);
$bOrig = ($rgbOrig & 0xFF);
$rgbBlur = ImageColorAt($imgBlur, $x, $y);
$rgbBlur = imagecolorat($imgBlur, $x, $y);
$rBlur = (($rgbBlur >> 16) & 0xFF);
$gBlur = (($rgbBlur >> 8) & 0xFF);
@@ -565,20 +637,20 @@ class ImageSizerEngineGD extends ImageSizerEngine {
: $bOrig;
if(($rOrig != $rNew) || ($gOrig != $gNew) || ($bOrig != $bNew)) {
$pixCol = ImageColorAllocate($img, $rNew, $gNew, $bNew);
ImageSetPixel($img, $x, $y, $pixCol);
$pixCol = imagecolorallocate($img, $rNew, $gNew, $bNew);
imagesetpixel($img, $x, $y, $pixCol);
}
}
}
} else {
for($x = 0; $x < $w; $x++) { // each row
for($y = 0; $y < $h; $y++) { // each pixel
$rgbOrig = ImageColorAt($img, $x, $y);
$rgbOrig = imagecolorat($img, $x, $y);
$rOrig = (($rgbOrig >> 16) & 0xFF);
$gOrig = (($rgbOrig >> 8) & 0xFF);
$bOrig = ($rgbOrig & 0xFF);
$rgbBlur = ImageColorAt($imgBlur, $x, $y);
$rgbBlur = imagecolorat($imgBlur, $x, $y);
$rBlur = (($rgbBlur >> 16) & 0xFF);
$gBlur = (($rgbBlur >> 8) & 0xFF);
@@ -603,7 +675,7 @@ class ImageSizerEngineGD extends ImageSizerEngine {
$bNew = 0;
}
$rgbNew = ($rNew << 16) + ($gNew << 8) + $bNew;
ImageSetPixel($img, $x, $y, $rgbNew);
imagesetpixel($img, $x, $y, $rgbNew);
}
}
}
@@ -721,11 +793,12 @@ class ImageSizerEngineGD extends ImageSizerEngine {
* @param array $sourceDimensions - array with three values: width, height, number of channels
* @param array|bool $targetDimensions - optional - mixed: bool true | false or array with three values:
* width, height, number of channels
* @param int|float Multiply needed memory by this factor
*
* @return bool|null if a calculation was possible (true|false), or null if the calculation could not be done
*
*/
static public function checkMemoryForImage($sourceDimensions, $targetDimensions = false) {
static public function checkMemoryForImage($sourceDimensions, $targetDimensions = false, $factor = 1) {
// with this static we only once need to read from php.ini and calculate phpMaxMem,
// regardless how often this function is called in a request
@@ -755,10 +828,8 @@ class ImageSizerEngineGD extends ImageSizerEngine {
}
// calculate $sourceDimensions
if(!isset($sourceDimensions[0]) || !isset($sourceDimensions[1])
|| !isset($sourceDimensions[2]) || !is_int($sourceDimensions[0])
|| !is_int($sourceDimensions[1]) || !is_int($sourceDimensions[2])
) {
if(!isset($sourceDimensions[0]) || !isset($sourceDimensions[1]) || !isset($sourceDimensions[2]) ||
!is_int($sourceDimensions[0]) || !is_int($sourceDimensions[1]) || !is_int($sourceDimensions[2])) {
return null;
}
@@ -771,10 +842,8 @@ class ImageSizerEngineGD extends ImageSizerEngine {
} else if(is_array($targetDimensions)) {
// we have to add ram for a targetimage
if(!isset($targetDimensions[0]) || !isset($targetDimensions[1])
|| !isset($targetDimensions[2]) || !is_int($targetDimensions[0])
|| !is_int($targetDimensions[1]) || !is_int($targetDimensions[2])
) {
if(!isset($targetDimensions[0]) || !isset($targetDimensions[1]) || !isset($targetDimensions[2]) ||
!is_int($targetDimensions[0]) || !is_int($targetDimensions[1]) || !is_int($targetDimensions[2])) {
return null;
}
@@ -785,7 +854,239 @@ class ImageSizerEngineGD extends ImageSizerEngine {
$curMem = memory_get_usage(true); // memory_get_usage() is always available with PHP since 5.2.1
// check if there is enough RAM loading the image(s), plus 3 MB for GD to use for calculations/transforms
return ($phpMaxMem - $curMem >= $imgMem + (3 * 1048576)) ? true : false;
$extraMem = 3 * 1048576;
$availableMem = $phpMaxMem - $curMem;
$neededMem = ($imgMem + $extraMem) * $factor;
return $availableMem >= $neededMem;
}
/**
* Additional functionality on top of existing checkMemoryForImage function for the flip/rotate actions
*
* @param string $filename Filename to check. Default is whatever was set to this ImageSizer.
* @param bool $double Need enough for both src and dst files loaded at same time? (default=true)
* @param int|float $factor Tweak factor (multiply needed memory by this factor), i.e. 2 for rotate actions. (default=1)
* @param string $action Name of action (if something other than "action")
* @param bool $throwIfNot Throw WireException if not enough memory? (default=false)
* @return bool
* @throws WireException
*
*/
protected function hasEnoughMemory($filename = '', $double = true, $factor = 1, $action = 'action', $throwIfNot = false) {
$error = '';
if(empty($filename)) $filename = $this->filename;
if($filename) {
if($filename != $this->filename || empty($this->info['width'])) {
$this->prepare($filename); // to populate $this->info
}
} else {
$error = 'No filename to check memory for';
}
if(!$error) {
$hasEnough = self::checkMemoryForImage(array(
$this->info['width'],
$this->info['height'],
$this->info['channels']
), $double, $factor);
if($hasEnough === false) {
$error = sprintf($this->_('Not enough memory for “%1$s” on image file: %2$s'), $action, basename($filename));
}
}
if($error) {
if($throwIfNot) {
throw new WireException($error);
} else {
$this->error($error);
return false;
}
}
return true;
}
/**
* Process a rotate or flip action
*
* @param string $srcFilename
* @param string $dstFilename
* @param string $action One of 'rotate' or 'flip'
* @param int|string $value If rotate, specify int of degrees. If flip, specify one of 'vertical', 'horizontal' or 'both'.
* @return bool
* @throws WireException
*
*/
private function processAction($srcFilename, $dstFilename, $action, $value) {
$action = strtolower($action);
$ext = strtolower(pathinfo($srcFilename, PATHINFO_EXTENSION));
$useTransparency = true;
$memFactor = 1;
$img = null;
if(empty($dstFilename)) $dstFilename = $srcFilename;
if($action == 'rotate') $memFactor *= 2;
if(!$this->hasEnoughMemory($srcFilename, true, $memFactor, $action, false)) return false;
if($ext == 'jpg' || $ext == 'jpeg') {
$img = imagecreatefromjpeg($srcFilename);
$useTransparency = false;
} else if($ext == 'png') {
$img = imagecreatefrompng($srcFilename);
} else if($ext == 'gif') {
$img = imagecreatefromgif($srcFilename);
}
if(!$img) {
$this->error("imagecreatefrom$ext failed", Notice::debug);
return false;
}
if($useTransparency) {
imagealphablending($img, true);
imagesavealpha($img, true);
}
$success = true;
$method = '_processAction' . ucfirst($action);
$imgNew = $this->$method($img, $value);
if($imgNew === false) {
// action fail
$success = false;
$this->error($this->className() . ".$method(img, $value) returned fail", Notice::debug);
} else if($imgNew !== $img) {
// a new img object was created
imagedestroy($img);
$img = $imgNew;
if($useTransparency) {
imagealphablending($img, true);
imagesavealpha($img, true);
}
} else {
// existing img object was updated
$img = $imgNew;
}
if($success) {
if($ext == 'png') {
$success = imagepng($img, $dstFilename, 9);
} else if($ext == 'gif') {
$success = imagegif($img, $dstFilename);
} else {
$success = imagejpeg($img, $dstFilename, $this->quality);
}
if(!$success) $this->error("image{$ext}() failed", Notice::debug);
}
imagedestroy($img);
return $success;
}
/**
* Process flip action (internal)
*
* @param resource $img
* @param string $flipType vertical, horizontal or both
* @return bool|resource
*
*/
private function _processActionFlip(&$img, $flipType) {
if(!function_exists('imageflip')) {
$this->error("Image flip requires PHP 5.5 or newer");
return false;
}
if(!in_array($flipType, array('vertical', 'horizontal', 'both'))) {
$this->error("Image flip type must be one of: 'vertical', 'horizontal', 'both'");
return false;
}
$constantName = 'IMG_FLIP_' . strtoupper($flipType);
$flipType = constant($constantName);
if($flipType === null) {
$this->error("Unknown constant for image flip: $constantName");
return false;
}
$success = imageflip($img, $flipType);
return $success ? $img : false;
}
/**
* Process rotate action (internal)
*
* @param resource $img
* @param $degrees
* @return bool|resource
*
*/
private function _processActionRotate(&$img, $degrees) {
$degrees = (int) $degrees;
$angle = 360 - $degrees; // imagerotate is anti-clockwise
$imgNew = imagerotate($img, $angle, 0);
return $imgNew ? $imgNew : false;
}
private function _processActionGreyscale(&$img, $unused) {
if($unused) {}
imagefilter($img, IMG_FILTER_GRAYSCALE);
return $img;
}
private function _processActionSepia(&$img, $sepia = 55) {
imagefilter($img, IMG_FILTER_GRAYSCALE);
imagefilter($img, IMG_FILTER_BRIGHTNESS, -30);
imagefilter($img, IMG_FILTER_COLORIZE, 90, (int) $sepia, 30);
return $img;
}
/**
* Process rotate of an image
*
* @param string $srcFilename
* @param string $dstFilename
* @param int $degrees Clockwise degrees, i.e. 90, 180, 270, -90, -180, -270
* @return bool
*
*/
protected function processRotate($srcFilename, $dstFilename, $degrees) {
return $this->processAction($srcFilename, $dstFilename, 'rotate', $degrees);
}
/**
* Process vertical or horizontal flip of an image
*
* @param string $srcFilename
* @param string $dstFilename
* @param string $flipType Specify vertical, horizontal, or both
* @return bool
*
*/
protected function processFlip($srcFilename, $dstFilename, $flipType) {
return $this->processAction($srcFilename, $dstFilename, 'flip', $flipType);
}
/**
* Convert image to greyscale
*
* @param string $dstFilename If different from source file
* @return bool
*
*/
public function convertToGreyscale($dstFilename = '') {
return $this->processAction($this->filename, $dstFilename, 'greyscale', null);
}
/**
* Convert image to sepia
*
* @param string $dstFilename If different from source file
* @param float|int $sepia Sepia value
* @return bool
*
*/
public function convertToSepia($dstFilename = '', $sepia = 55) {
return $this->processAction($this->filename, $dstFilename, 'sepia', $sepia);
}
}

View File

@@ -68,8 +68,10 @@
* @property string $requiredIf Optional conditions under which input is required (selector string). #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 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|bool $entityEncodeLabel Set to boolean false to specifically disable entity encoding of field header/label (default=true). #pw-group-output
* @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 $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
@@ -80,6 +82,7 @@
* ================
* @method string render()
* @method string renderValue()
* @method void renderReadyHook(Inputfield $parent, $renderValueMode)
* @method Inputfield processInput(WireInputData $input)
* @method InputfieldWrapper getConfigInputfields()
* @method array getConfigArray()
@@ -385,7 +388,7 @@ abstract class Inputfield extends WireData implements Module {
*
* @param string $key Name of property to set
* @param mixed $value Value of property
* @return $this
* @return Inputfield|WireData
*
*/
public function set($key, $value) {
@@ -642,7 +645,7 @@ abstract class Inputfield extends WireData implements Module {
* - String with attributes split by "+" or "|" to set them all to have the same value.
* - Specify boolean true to get all attributes in an associative array.
* @param string|int|null $value Value to set (if setting), omit otherwise.
* @return mixed|$this If setting an attribute, it returns this instance. If getting an attribute, the attribute is returned.
* @return Inputfield|array|string|int|object|float If setting an attribute, it returns this instance. If getting an attribute, the attribute is returned.
* @see Inputfield::removeAttr(), Inputfield::addClass(), Inputfield::removeClass()
*
*/
@@ -658,6 +661,25 @@ abstract class Inputfield extends WireData implements Module {
}
return $this->setAttribute($key, $value);
}
/**
* Shortcut for getting or setting “value” attribute
*
* When setting a value, it returns $this (for fluent interface).
*
* ~~~~~
* $value = $inputfield->val(); * // Getting
* $inputfield->val('foo'); * // Setting
* ~~~~~
*
* @param string|null $value
* @return string|int|float|array|object|Wire|WireData|WireArray|Inputfield
*
*/
public function val($value = null) {
if($value === null) return $this->getAttribute('value');
return $this->setAttribute('value', $value);
}
/**
* Get all attributes specified for this Inputfield
@@ -702,7 +724,7 @@ abstract class Inputfield extends WireData implements Module {
* - Omit if getting an attribute.
* - Value to set for $key of setting.
* - Boolean false to remove the attribute specified for $key.
* @return string|array|$this Returns one of the following:
* @return Inputfield|string|array|null Returns one of the following:
* - If getting, returns attribute value of NULL if not present.
* - If setting, returns $this.
* @see Inputfield::attr(), Inputfield::addClass()
@@ -788,12 +810,13 @@ abstract class Inputfield extends WireData implements Module {
foreach($addClasses as $addClass) {
$addClass = trim($addClass);
if(!strlen($addClass)) continue;
if(in_array($addClass, $classes)) continue; // if already present, don't add it
$classes[] = $addClass;
}
$classes = array_unique($classes);
// convert back to string
$value = implode(' ', $classes);
$value = trim(implode(' ', $classes));
// set back to Inputfield
if($property == 'class') {
@@ -1030,9 +1053,24 @@ abstract class Inputfield extends WireData implements Module {
public function renderReady(Inputfield $parent = null, $renderValueMode = false) {
if($parent) {}
if($renderValueMode) {}
return $this->wire('modules')->loadModuleFileAssets($this) > 0;
$result = $this->wire('modules')->loadModuleFileAssets($this) > 0;
if($this->wire('hooks')->isMethodHooked($this, 'renderReadyHook')) {
$this->renderReadyHook($parent, $renderValueMode);
}
return $result;
}
/**
* Hookable version of renderReady(), not called unless 'renderReadyHook' is hooked
*
* Hook this method instead if you want to hook renderReady().
*
* @param Inputfield $parent
* @param bool $renderValueMode
*
*/
public function ___renderReadyHook(Inputfield $parent = null, $renderValueMode) { }
/**
* This hook was replaced by renderReady
*
@@ -1237,7 +1275,7 @@ abstract class Inputfield extends WireData implements Module {
$field = $this->modules->get('InputfieldInteger');
$value = (int) $this->getSetting('columnWidth');
if($value < 10 || $value >= 100) $value = 100;
$field->label = sprintf($this->_("Column Width (%d%%)"), $value);
$field->label = sprintf($this->_('Column width (%d%%)'), $value);
$field->icon = 'arrows-h';
$field->attr('id+name', 'columnWidth');
$field->attr('type', 'text');
@@ -1502,7 +1540,7 @@ abstract class Inputfield extends WireData implements Module {
* @param string $what Name of property that changed
* @param mixed $old Previous value before change
* @param mixed $new New value
* @return $this
* @return Inputfield|WireData $this
*
*/
public function trackChange($what, $old = null, $new = null) {
@@ -1531,7 +1569,7 @@ abstract class Inputfield extends WireData implements Module {
public function entityEncode($str, $markdown = false) {
// if already encoded, then un-encode it
if(strpos($str, '&') !== false && preg_match('/&(#\d+|[a-z]+);/', $str)) {
if(strpos($str, '&') !== false && preg_match('/&(#\d+|[a-zA-Z]+);/', $str)) {
$str = html_entity_decode($str, ENT_QUOTES, "UTF-8");
}

View File

@@ -42,15 +42,15 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
*
*/
static protected $defaultMarkup = array(
'list' => "\n<ul {attrs}>\n{out}\n</ul>\n",
'item' => "\n\t<li {attrs}>\n{out}\n\t</li>",
'item_label' => "\n\t\t<label class='InputfieldHeader ui-widget-header{class}' for='{for}'>{out}</label>",
'item_label_hidden' => "\n\t\t<label class='InputfieldHeader InputfieldHeaderHidden ui-widget-header{class}'><span>{out}</span></label>",
'item_content' => "\n\t\t<div class='InputfieldContent ui-widget-content{class}'>\n{out}\n\t\t</div>",
'item_error' => "\n<p class='InputfieldError ui-state-error'><i class='fa fa-fw fa-flash'></i><span>{out}</span></p>",
'item_description' => "\n<p class='description'>{out}</p>",
'item_head' => "\n<h2>{out}</h2>",
'item_notes' => "\n<p class='notes'>{out}</p>",
'list' => "<ul {attrs}>{out}</ul>",
'item' => "<li {attrs}>{out}</li>",
'item_label' => "<label class='InputfieldHeader ui-widget-header{class}' for='{for}'>{out}</label>",
'item_label_hidden' => "<label class='InputfieldHeader InputfieldHeaderHidden ui-widget-header{class}'><span>{out}</span></label>",
'item_content' => "<div class='InputfieldContent ui-widget-content{class}'>{out}</div>",
'item_error' => "<p class='InputfieldError ui-state-error'><i class='fa fa-fw fa-flash'></i><span>{out}</span></p>",
'item_description' => "<p class='description'>{out}</p>",
'item_head' => "<h2>{out}</h2>",
'item_notes' => "<p class='notes'>{out}</p>",
'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:
@@ -101,6 +101,14 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
*/
protected $requiredLabel = '';
/**
* Whether or not column width is handled internally
*
* @var bool
*
*/
protected $useColumnWidth = true;
/**
* Construct the Inputfield, setting defaults for all properties
*
@@ -119,6 +127,8 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
if(is_array($settings)) foreach($settings as $key => $value) {
if($key == 'requiredLabel') {
$this->requiredLabel = $value;
} else if($key == 'useColumnWidth') {
$this->useColumnWidth = $value;
} else {
$this->set($key, $value);
}
@@ -368,7 +378,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
$_classes = array_merge(self::$defaultClasses, self::$classes);
$markup = array();
$classes = array();
$useColumnWidth = true;
$useColumnWidth = $this->useColumnWidth;
$renderAjaxInputfield = $this->wire('config')->ajax ? $this->wire('input')->get('renderInputfieldAjax') : null;
if(isset($_classes['form']) && strpos($_classes['form'], 'InputfieldFormNoWidths') !== false) {
@@ -504,7 +514,13 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
if($label || $quietMode) {
$for = $inputfield->getSetting('skipLabel') || $quietMode ? '' : $inputfield->attr('id');
// if $inputfield has a property of entityEncodeLabel with a value of boolean FALSE, we don't entity encode
if($inputfield->getSetting('entityEncodeLabel') !== false) $label = $inputfield->entityEncode($label);
$entityEncodeLabel = $inputfield->getSetting('entityEncodeLabel');
if(is_int($entityEncodeLabel) && $entityEncodeLabel >= Inputfield::textFormatBasic) {
// uses an Inputfield::textFormat constant
$label = $inputfield->entityEncode($label, $entityEncodeLabel);
} else if($entityEncodeLabel !== false) {
$label = $inputfield->entityEncode($label);
}
$icon = $inputfield->getSetting('icon');
$icon = $icon ? str_replace('{name}', $this->wire('sanitizer')->name(str_replace(array('icon-', 'fa-'), '', $icon)), $markup['item_icon']) : '';
$toggle = $collapsed == Inputfield::collapsedNever ? '' : $markup['item_toggle'];
@@ -528,20 +544,25 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
} else if(strpos($label, '{class}') !== false) {
$label = str_replace('{class}', '', $label);
}
} else {
// no header
// $inputfield->addClass('InputfieldNoHeader', 'wrapClass');
}
if($useColumnWidth) {
$columnWidth = (int) $inputfield->getSetting('columnWidth');
$columnWidthAdjusted = $columnWidth + ($columnWidthTotal ? -1 * $columnWidthSpacing : 0);
if($columnWidth >= 9 && $columnWidth <= 100) {
$ffAttrs['class'] .= ' ' . $classes['item_column_width'];
if(!$columnWidthTotal) $ffAttrs['class'] .= ' ' . $classes['item_column_width_first'];
$columnWidth = (int) $inputfield->getSetting('columnWidth');
$columnWidthAdjusted = $columnWidth + ($columnWidthTotal ? -1 * $columnWidthSpacing : 0);
if($columnWidth >= 9 && $columnWidth <= 100) {
$ffAttrs['class'] .= ' ' . $classes['item_column_width'];
if(!$columnWidthTotal) $ffAttrs['class'] .= ' ' . $classes['item_column_width_first'];
if($useColumnWidth) {
$ffAttrs['style'] = "width: $columnWidthAdjusted%;";
$columnWidthTotal += $columnWidth;
//if($columnWidthTotal >= 100 && !$requiredIf) $columnWidthTotal = 0; // requiredIf meant to be a showIf?
if($columnWidthTotal >= 100) $columnWidthTotal = 0;
} else {
$columnWidthTotal = 0;
$ffAttrs['data-colwidth'] = "$columnWidthAdjusted%";
}
$columnWidthTotal += $columnWidth;
//if($columnWidthTotal >= 100 && !$requiredIf) $columnWidthTotal = 0; // requiredIf meant to be a showIf?
if($columnWidthTotal >= 100) $columnWidthTotal = 0;
} else {
$columnWidthTotal = 0;
}
if(!isset($ffAttrs['id'])) $ffAttrs['id'] = 'wrap_' . $inputfield->attr('id');
$ffAttrs['class'] = str_replace('Inputfield_ ', '', $ffAttrs['class']);
@@ -688,23 +709,8 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
return $this->renderInputfield($in, $renderValueMode);
} else {
// do not render ajax inputfield
$url = $this->wire('input')->url();
$queryString = $this->wire('input')->queryString();
if(strpos($queryString, 'renderInputfieldAjax=') !== false) {
// in case nested ajax request
$queryString = preg_replace('/&?renderInputfieldAjax=[^&]+/', '', $queryString);
}
$url .= $queryString ? "?$queryString&" : "?";
$url .= "renderInputfieldAjax=$inputfieldID";
$out = "<div class='renderInputfieldAjax'><input type='hidden' value='$url' /></div>";
if($inputfield instanceof InputfieldWrapper) {
// load assets they will need
foreach($inputfield->getAll() as $in) {
$in->renderReady($inputfield, $renderValueMode);
}
}
return $out;
// do not render ajax inputfield now, instead render placeholder
return $this->renderInputfieldAjaxPlaceholder($inputfield, $renderValueMode);
}
}
@@ -721,6 +727,45 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
return $out;
}
/**
* Render a placeholder for an ajax-loaded Inputfield
*
* @param Inputfield $inputfield
* @param bool $renderValueMode
* @return string
*
*/
protected function renderInputfieldAjaxPlaceholder(Inputfield $inputfield, $renderValueMode) {
$inputfieldID = $inputfield->attr('id');
$url = $this->wire('input')->url();
$queryString = $this->wire('input')->queryString();
if(strpos($queryString, 'renderInputfieldAjax=') !== false) {
// in case nested ajax request
$queryString = preg_replace('/&?renderInputfieldAjax=[^&]+/', '', $queryString);
}
$url .= $queryString ? "?$queryString&" : "?";
$url .= "renderInputfieldAjax=$inputfieldID";
$out = "<div class='renderInputfieldAjax'><input type='hidden' value='$url' /></div>";
if($inputfield instanceof InputfieldWrapper) {
// load assets they will need
foreach($inputfield->getAll() as $in) {
$in->renderReady($inputfield, $renderValueMode);
}
}
// ensure that Inputfield::render() hooks are still called
if($inputfield->hasHook('render()')) {
$inputfield->runHooks('render', array(), 'before');
}
return $out;
}
/**
* Process input for all children
*
@@ -900,7 +945,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
* #pw-internal
*
* @param bool $clear Set to true in order to clear the delayed children list.
* @return array
* @return array|Inputfield[]
*
*/
public function _getDelayedChildren($clear = false) {

View File

@@ -443,6 +443,68 @@ interface LanguagesValueInterface {
}
/**
* Interface used to indicate that the Fieldtype supports multiple languages
*
*/
interface FieldtypeLanguageInterface {
/*
* This interface is symbolic only and doesn't require any additional methods,
* however you do need to add an 'implements FieldtypeLanguageInterface' when defining your class.
*
*/
}
/**
* Interface for objects that carry a Field value for a Page
*
* Optional, but enables Page to do some of the work rather than the Fieldtype
*
*/
interface PageFieldValueInterface {
/**
* Get or set formatted state
*
* @param bool|null $set Specify bool to set formatted state or omit to retrieve formatted state
* @return bool
*
*/
public function formatted($set = null);
/**
* Set the Page
*
* @param Page $page
*
*/
public function setPage(Page $page);
/**
* Set the Field
*
* @param Field $field
*
*/
public function setField(Field $field);
/**
* Get the page or null if not set
*
* @return Page|null
*
*/
public function getPage();
/**
* Get the field or null if not set
*
* @return Field|null
*
*/
public function getField();
}
/**
* Interface for tracking runtime events
*
@@ -486,3 +548,10 @@ interface WireProfilerInterface {
*/
interface InputfieldHasArrayValue { }
/**
* Inputfield that has a sortable value (usually in addition to InputfieldHasArrayValue)
*
*/
interface InputfieldHasSortableValue { }

View File

@@ -36,13 +36,18 @@ function __($text, $textdomain = null, $context = '') {
$textdomain = $traces[1]['file'];
}
if(is_null($textdomain)) $textdomain = 'site';
} else if($textdomain === 'common') {
// common translation
$textdomain = 'wire/modules/LanguageSupport/LanguageTranslator.php';
}
$value = htmlspecialchars($language->translator()->getTranslation($textdomain, $text, $context), ENT_QUOTES, 'UTF-8');
$value = $language->translator()->getTranslation($textdomain, $text, $context);
if($value === "=") {
$value = $text;
} else if($value === "+") {
$v = $language->translator()->commonTranslation($text);
$value = empty($v) ? $text : $v;
} else {
$value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false);
}
return $value;
}

View File

@@ -115,8 +115,22 @@ class MarkupFieldtype extends WireData implements Module {
$page = $value;
$value = $page->getFormatted($property);
$field = $this->wire('fields')->get($property);
if($field && $field->type) return $field->type->markupValue($page, $field, $value);
if(is_object($field) && $field->type) return $field->type->markupValue($page, $field, $value);
$valid = true;
} else if($value instanceof LanguagesValueInterface) {
/** @var LanguaagesValueInterface $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];
}
$value = $value->getLanguageValue($languageID);
} else {
$value = (string) $value;
}
} else if($value instanceof WireData) {
// WireData object

View File

@@ -73,7 +73,10 @@ class MarkupQA extends Wire {
if($page) $this->setPage($page);
if($field) $this->setField($field);
$this->assetsURL = $this->wire('config')->urls->assets;
$this->debug = (bool) $this->wire('config')->debugMarkupQA && $this->wire('user')->isSuperuser();
if($this->wire('config')->debugMarkupQA) {
$user = $this->wire('user');
if($user) $this->debug = $user->isSuperuser();
}
}
/**
@@ -436,6 +439,10 @@ class MarkupQA extends Wire {
$langName = $this->debug && $language ? $language->name : '';
if($livePath) {
if($path && substr($path, -1) != '/') {
// no trailing slash, retain the editors wishes here
$livePath = rtrim($livePath, '/');
}
if(strpos($livePath, '/trash/') !== false) {
// linked page is in trash, we won't update it but we'll produce a warning
$this->linkWarning("$path => $livePath (" . $this->_('it is in the trash') . ')');

View File

@@ -14,6 +14,17 @@
* https://processwire.com
*
* #pw-summary Loads and manages all modules in ProcessWire.
* #pw-body =
* The `$modules` API variable is most commonly used for getting individual modules to use their API.
* ~~~~~
* // Getting a module by name
* $m = $modules->get('MarkupPagerNav');
*
* // Getting a module by name (alternate)
* $m = $modules->MarkupPagerNav;
* ~~~~~
*
* #pw-body
*
* @todo Move all module information methods to a ModulesInfo class
* @todo Move all module loading methods to a ModulesLoad class
@@ -23,7 +34,7 @@
* @method bool|int delete($class)
* @method bool uninstall($class)
* @method bool saveModuleConfigData($className, array $configData) Alias of saveConfig() method #pw-internal
* @method bool saveConfig($class, array $data)
* @method bool saveConfig($class, $data, $value = null)
* @method InputfieldWrapper|null getModuleConfigInputfields($moduleName, InputfieldWrapper $form = null) #pw-internal
* @method void moduleVersionChanged(Module $module, $fromVersion, $toVersion) #pw-internal
*
@@ -556,14 +567,19 @@ class Modules extends WireArray {
* Initialize a single module
*
* @param Module $module
* @param bool $clearSettings If true, module settings will be cleared when appropriate to save space.
* @param array $options
* - `clearSettings` (bool): When true, module settings will be cleared when appropriate to save space. (default=true)
* - `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.
*
*/
protected function initModule(Module $module, $clearSettings = true) {
protected 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;
@@ -590,6 +606,7 @@ class Modules extends WireArray {
try {
$module->init();
} catch(\Exception $e) {
if($throw) throw($e);
$this->error(sprintf($this->_('Failed to init module: %s'), $moduleName) . " - " . $e->getMessage());
$result = false;
}
@@ -731,8 +748,6 @@ class Modules extends WireArray {
/**
* Retrieve the installed module info as stored in the database
*
* @return array Indexed by module class name => array of module info
*
*/
protected function loadModulesTable() {
$database = $this->wire('database');
@@ -1156,13 +1171,15 @@ class Modules extends WireArray {
* Get the requested Module (with options)
*
* This is the same as `$modules->get()` except that you can specify additional options to modify default behavior.
* These are the options you can speicfy in the `$options` array argument:
* These are the options you can specify in the `$options` array argument:
*
* - `noPermissionCheck` (bool): Specify true to disable module permission checks (and resulting exception).
* - `noInstall` (bool): Specify true to prevent a non-installed module from installing from this request.
* - `noInit` (bool): Specify true to prevent the module from being initialized.
* - `noSubstitute` (bool): Specify true to prevent inclusion of a substitute module.
* - `noCache` (bool): Specify true to prevent module instance from being cached for later getModule() calls.
* - `noPermissionCheck` (bool): Specify true to disable module permission checks (and resulting exception). (default=false)
* - `noInstall` (bool): Specify true to prevent a non-installed module from installing from this request. (default=false)
* - `noInit` (bool): Specify true to prevent the module from being initialized. (default=false)
* - `noSubstitute` (bool): Specify true to prevent inclusion of a substitute module. (default=false)
* - `noCache` (bool): Specify true to prevent module instance from being cached for later getModule() calls. (default=false)
* - `noThrow` (bool): Specify true to prevent exceptions from being thrown on permission or fatal error. (default=false)
* - `returnError` (bool): Return an error message (string) on error, rather than null. (default=false)
*
* If the module is not installed, but is installable, it will be installed, instantiated, and initialized.
* If you don't want that behavior, call `$modules->isInstalled('ModuleName')` as a condition first, OR specify
@@ -1170,69 +1187,111 @@ class Modules extends WireArray {
*
* @param string|int $key Module name or database ID.
* @param array $options Optional settings to change load behavior, see method description for details.
* @return Module|_Module|null Returns ready-to-use module or NULL if not found.
* @throws WirePermissionException If module requires a particular permission the user does not have
* @return Module|_Module|null|string Returns ready-to-use module or NULL|string if not found (string if `returnError` option used).
* @throws WirePermissionException|\Exception If module requires a particular permission the user does not have
* @see Modules::get()
*
*/
public function getModule($key, array $options = array()) {
if(empty($key)) return null;
$module = null;
$needsInit = false;
$error = '';
if(empty($key)) {
return empty($options['returnError']) ? null : "No module specified";
}
// check for optional module ID and convert to classname if found
if(ctype_digit("$key")) {
if(!$key = array_search($key, $this->moduleIDs)) return null;
$moduleID = (int) $key;
if(!$key = array_search($key, $this->moduleIDs)) {
return empty($options['returnError']) ? null : "Unable to find module ID $moduleID";
}
} else {
$key = wireClassName($key, false);
}
$module = parent::get($key);
if(!$module && empty($options['noSubstitute'])) {
if($this->isInstallable($key) && empty($options['noInstall'])) {
// module is on file system and may be installed, no need to substitute
if(!$module) {
if(empty($options['noSubstitute'])) {
if($this->isInstallable($key) && empty($options['noInstall'])) {
// module is on file system and may be installed, no need to substitute
} else {
$module = $this->getSubstituteModule($key, $options);
if($module) return $module; // returned module is ready to use
}
} else {
$module = $this->getSubstituteModule($key, $options);
if($module) return $module; // returned module is ready to use
$error = "Module '$key' not found and substitute not allowed (noSubstitute=true)";
}
}
if($module) {
// check if it's a placeholder, and if it is then include/instantiate/init the real module
// OR check if it's non-singular, so that a new instance is created
if($module instanceof ModulePlaceholder || !$this->isSingular($module)) {
$placeholder = $module;
$class = $this->getModuleClass($placeholder);
if($module instanceof ModulePlaceholder) $this->includeModule($module);
$module = $this->newModule($class);
try {
if($module instanceof ModulePlaceholder) $this->includeModule($module);
$module = $this->newModule($class);
} catch(\Exception $e) {
if(empty($options['noThrow'])) throw $e;
return empty($options['returnError']) ? null : "Module '$key' - " . $e->getMessage();
}
// if singular, save the instance so it can be used in later calls
if($module && $this->isSingular($module) && empty($options['noCache'])) $this->set($key, $module);
$needsInit = true;
}
} else if(empty($options['noInstall']) && array_key_exists($key, $this->getInstallable())) {
// check if the request is for an uninstalled module
// if so, install it and return it
$module = $this->install($key);
$needsInit = true;
} else if(empty($options['noInstall'])) {
// module was not available to get, see if we can install it
if(array_key_exists($key, $this->getInstallable())) {
// check if the request is for an uninstalled module
// if so, install it and return it
try {
$module = $this->install($key);
} catch(\Exception $e) {
if(empty($options['noThrow'])) throw $e;
if(!empty($options['returnError'])) return "Module '$key' install failed: " . $e->getMessage();
}
$needsInit = true;
if(!$module) $error = "Module '$key' not installed and install failed";
} else {
$error = "Module '$key' is not present or listed as installable";
}
} else {
$error = "Module '$key' is not present and not installable (noInstall=true)";
}
if($module && empty($options['noPermissionCheck'])) {
if(!$module) {
if(!$error) $error = "Unable to get module '$key'";
return empty($options['returnError']) ? null : $error;
}
if(empty($options['noPermissionCheck'])) {
// check that user has permission required to use module
if(!$this->hasPermission($module, $this->wire('user'), $this->wire('page'))) {
throw new WirePermissionException($this->_('You do not have permission to execute this module') . ' - ' . wireClassName($module));
$error = $this->_('You do not have permission to execute this module') . ' - ' . wireClassName($module);
if(empty($options['noThrow'])) throw new WirePermissionException($error);
return empty($options['returnError']) ? null : $error;
}
}
// skip autoload modules because they have already been initialized in the load() method
// unless they were just installed, in which case we need do init now
if($module && $needsInit) {
if($needsInit && empty($options['noInit'])) {
// if the module is configurable, then load it's config data
// and set values for each before initializing the module
if(empty($options['noInit'])) {
if(!$this->initModule($module, false)) $module = null;
try {
if(!$this->initModule($module, array('clearSettings' => false, 'throw' => true))) {
return empty($options['returnError']) ? null : "Module '$module' failed init";
$module = null;
}
} catch(\Exception $e) {
if(empty($options['noThrow'])) throw $e;
return empty($options['returnError']) ? null : "Module '$module' throw Exception on init - " . $e->getMessage();
}
}
@@ -2456,7 +2515,8 @@ class Modules extends WireArray {
* $moduleInfo = $modules->getModuleInfoVerbose('MarkupAdminDataTable');
* ~~~~~
*
* @param string|Module|int $class May be class name, module instance, or module ID
* @param string|Module|int $class May be class name, module instance, or module ID.
* Specify "*" or "all" to retrieve module info for all modules.
* @param array $options Optional options to modify behavior of what gets returned
* - `verbose` (bool): Makes the info also include additional properties (they will be usually blank without this option specified).
* - `noCache` (bool): prevents use of cache to retrieve the module info.
@@ -2544,11 +2604,22 @@ class Modules extends WireArray {
if(!count($info)) $info = $this->getModuleInfoInternal($module);
}
} else if($module == 'PHP' || $module == 'ProcessWire') {
} else if($module == 'PHP' || $module == 'ProcessWire') {
// module is a system
$info = $this->getModuleInfoSystem($module);
$info = $this->getModuleInfoSystem($module);
return array_merge($infoTemplate, $info);
} else if($module === '*' || $module === 'all') {
if(empty($this->moduleInfoCache)) $this->loadModuleInfoCache();
$modulesInfo = $this->moduleInfoCache;
if($options['verbose']) {
if(empty($this->moduleInfoCacheVerbose)) $this->loadModuleInfoCacheVerbose();
foreach($this->moduleInfoCacheVerbose as $moduleID => $moduleInfoVerbose) {
$modulesInfo[$moduleID] = array_merge($modulesInfo[$moduleID], $moduleInfoVerbose);
}
}
return $modulesInfo;
} else {
// module is a class name or ID
@@ -2625,8 +2696,11 @@ class Modules extends WireArray {
// if $info[requires] or $info[installs] isn't already an array, make it one
if(!is_array($info['requires'])) {
$info['requires'] = str_replace(' ', '', $info['requires']); // remove whitespace
if(strpos($info['requires'], ',') !== false) $info['requires'] = explode(',', $info['requires']);
else $info['requires'] = array($info['requires']);
if(strpos($info['requires'], ',') !== false) {
$info['requires'] = explode(',', $info['requires']);
} else {
$info['requires'] = array($info['requires']);
}
}
// populate requiresVersions
@@ -2646,8 +2720,11 @@ class Modules extends WireArray {
// what does it install?
if(!is_array($info['installs'])) {
$info['installs'] = str_replace(' ', '', $info['installs']); // remove whitespace
if(strpos($info['installs'], ',') !== false) $info['installs'] = explode(',', $info['installs']);
else $info['installs'] = array($info['installs']);
if(strpos($info['installs'], ',') !== false) {
$info['installs'] = explode(',', $info['installs']);
} else {
$info['installs'] = array($info['installs']);
}
}
// misc
@@ -4705,6 +4782,8 @@ class Modules extends WireArray {
/**
* Compile and return the given file for module, if allowed to do so
*
* #pw-internal
*
* @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

View File

@@ -3,6 +3,10 @@
/**
* ProcessWire Notices
*
* #pw-summary Manages notifications in the ProcessWire admin, primarily for internal use.
* #pw-use-constants
* #pw-use-constructor
*
* Base class that holds a message, source class, and timestamp.
* Contains notices/messages used by the application to the user.
*
@@ -28,6 +32,8 @@ abstract class Notice extends WireData {
/**
* Flag indicates the notice is a warning
*
* #pw-internal
*
* @deprecated use NoticeWarning instead.
*
*/
@@ -46,7 +52,7 @@ abstract class Notice extends WireData {
const logOnly = 16;
/**
* Flag indicates the notice is allowed to contain markup and won't be automatically entity encoded
* 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.
*
@@ -56,8 +62,8 @@ abstract class Notice extends WireData {
/**
* Create the Notice
*
* @param string $text
* @param int $flags
* @param string $text Notification text
* @param int $flags Flags
*
*/
public function __construct($text, $flags = 0) {
@@ -69,6 +75,8 @@ abstract class Notice extends WireData {
}
/**
* Get the notice log
*
* @return string Name of log (basename)
*
*/
@@ -111,21 +119,88 @@ class NoticeWarning extends Notice {
/**
* A class to contain multiple Notice instances, whether messages or errors
* ProcessWire Notices
*
* #pw-summary A class to contain multiple Notice instances, whether messages, warnings or errors
* #pw-body =
* This class manages notices that have been sent by `Wire::message()`, `Wire::warning()` and `Wire::error()` calls.
* The message(), warning() and error() methods are available on every `Wire` derived object. This class is primarily
* for internal use in the admin. However, it may also be useful in some front-end contexts.
* ~~~~~
* // Adding a NoticeMessage using object syntax
* $notices->add(new NoticeMessage("Hello World"));
*
* // Adding a NoticeMessage using regular syntax
* $notices->message("Hello World");
*
* // Adding a NoticeWarning, and allow markup in it
* $notices->message("Hello <strong>World</strong>", Notice::allowMarkup);
*
* // Adding a NoticeError that only appears if debug mode is on
* $notices->error("Hello World", Notice::debug);
* ~~~~~
* Iterating and outputting Notices:
* ~~~~~
* foreach($notices as $notice) {
* // skip over debug notices, if debug mode isn't active
* if($notice->flags & Notice::debug && !$config->debug) continue;
* // entity encode notices unless the allowMarkup flag is set
* if($notice->flags & Notice::allowMarkup) {
* $text = $notice->text;
* } else {
* $text = $sanitizer->entities($notice->text);
* }
* // output either an error, warning or message notice
* if($notice instanceof NoticeError) {
* echo "<p class='error'>$text</p>";
* } else if($notice instanceof NoticeWarning) {
* echo "<p class='warning'>$text</p>";
* } else {
* echo "<p class='message'>$text</p>";
* }
* }
* ~~~~~
*
* #pw-body
*
*
*/
class Notices extends WireArray {
const logAllNotices = false; // for debugging/dev purposes
/**
* #pw-internal
*
* @param mixed $item
* @return bool
*
*/
public function isValidItem($item) {
return $item instanceof Notice;
}
}
/**
* #pw-internal
*
* @return Notice
*
*/
public function makeBlankItem() {
return $this->wire(new NoticeMessage(''));
}
/**
* Add a Notice object
*
* ~~~~
* $notices->add(new NoticeError("An error occurred!"));
* ~~~~
*
* @param Notice $item
* @return $this
*
*/
public function add($item) {
if($item->flags & Notice::debug) {
@@ -178,6 +253,12 @@ class Notices extends WireArray {
$this->wire('log')->save($item->getName(), $text);
}
/**
* Are there NoticeError items present?
*
* @return bool
*
*/
public function hasErrors() {
$numErrors = 0;
foreach($this as $notice) {
@@ -185,7 +266,13 @@ class Notices extends WireArray {
}
return $numErrors > 0;
}
/**
* Are there NoticeWarning items present?
*
* @return bool
*
*/
public function hasWarnings() {
$numWarnings = 0;
foreach($this as $notice) {
@@ -199,6 +286,8 @@ class Notices extends WireArray {
*
* This enables us to safely print_r the string for debugging purposes
*
* #pw-internal
*
* @param array $a
* @return array
*
@@ -218,4 +307,32 @@ class Notices extends WireArray {
}
return $b;
}
/**
* Move notices from one Wire instance to another
*
* @param Wire $from
* @param Wire $to
* @param array $options Additional options:
* - `types` (array): Types to move (default=['messages','warnings','errors'])
* - `prefix` (string): Optional prefix to add to moved notices text (default='')
* - `suffix` (string): Optional suffix to add to moved notices text (default='')
* @return int Number of notices moved
*
*/
public function move(Wire $from, Wire $to, array $options = array()) {
$n = 0;
$types = isset($options['types']) ? $options['types'] : array('errors', 'warnings', 'messages');
foreach($types as $type) {
$method = rtrim($type, 's');
foreach($from->$type('clear') as $notice) {
$text = $notice->text;
if(isset($options['prefix'])) $text = "$options[prefix]$text";
if(isset($options['suffix'])) $text = "$text$options[suffix]";
$to->$method($text, $notice->flags);
$n++;
}
}
return $n;
}
}

View File

@@ -44,10 +44,11 @@ class NullPage extends Page {
/**
* #pw-internal
*
* @param array $options
* @return string
*
*/
public function url() { return ''; }
public function url($options = array()) { return ''; }
/**
* #pw-internal

View File

@@ -8,7 +8,7 @@
* 1. Providing get/set access to the Page's properties
* 2. Accessing the related hierarchy of pages (i.e. parents, children, sibling pages)
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2017 by Ryan Cramer
* https://processwire.com
*
* #pw-summary Class used by all Page objects in ProcessWire.
@@ -16,6 +16,12 @@
* #pw-summary-system Most system properties directly correspond to columns in the `pages` database table.
* #pw-order-groups common,traversal,manipulation,date-time,access,output-rendering,status,constants,languages,system,advanced,hooks
* #pw-use-constants
* #pw-var $page
* #pw-body =
* The `$page` API variable represents the current page being viewed. However, the documentation
* here also applies to all Page objects that you may work with in the API. We use `$page` as the most common example
* throughout the documentation, but you can substitute that with any variable name representing a Page.
* #pw-body
*
* @link http://processwire.com/api/ref/page/ Offical $page Documentation
* @link http://processwire.com/api/selectors/ Official Selectors Documentation
@@ -37,7 +43,8 @@
* @property Template|null $templatePrevious Previous template, if template was changed. #pw-advanced
* @property Fieldgroup $fields All the Fields assigned to this page (via its template). Returns a Fieldgroup. #pw-advanced
* @property int $numChildren The number of children (subpages) this page has, with no exclusions (fast). #pw-group-traversal
* @property int $numVisibleChildren The number of visible children this page has. Excludes unpublished, no-access, hidden, etc. #pw-group-traversal
* @property int $hasChildren The number of visible children this page has. Excludes unpublished, no-access, hidden, etc. #pw-group-traversal
* @property int $numVisibleChildren Verbose alias of $hasChildren #pw-internal
* @property PageArray $children All the children of this page. Returns a PageArray. See also $page->children($selector). #pw-group-traversal
* @property Page|NullPage $child The first child of this page. Returns a Page. See also $page->child($selector). #pw-group-traversal
* @property PageArray $siblings All the sibling pages of this page. Returns a PageArray. See also $page->siblings($selector). #pw-group-traversal
@@ -66,7 +73,9 @@
* @property string $editUrl URL that this page can be edited at. #pw-group-advanced
* @property string $editURL Alias of $editUrl. #pw-internal
* @property PageRender $render May be used for field markup rendering like $page->render->title. #pw-advanced
* @property bool $loaderCache Whether or not pages loaded as a result of this one may be cached by PagesLoaderCache. #pw-internal
*
* @property Page|null $_cloning Internal runtime use, contains Page being cloned (source), when this Page is the new copy (target). #pw-internal
* @property bool|null $_hasAutogenName Internal runtime use, set by Pages class when page as auto-generated name. #pw-internal
* @property bool|null $_forceSaveParents Internal runtime/debugging use, force a page to refresh its pages_parents DB entries on save(). #pw-internal
*
@@ -76,16 +85,26 @@
*
* Methods added by PagePermissions.module:
* ----------------------------------------
* @method bool viewable($field = '') Returns true if the page (and optionally field) is viewable by the current user, false if not. #pw-group-access
* @method bool editable($field = '') Returns true if the page (and optionally field) is editable by the current user, false if not. #pw-group-access
* @method bool viewable($field = '', $checkTemplateFile = true) Returns true if the page (and optionally field) is viewable by the current user, false if not. #pw-group-access
* @method bool editable($field = '', $checkPageEditable = true) Returns true if the page (and optionally field) is editable by the current user, false if not. #pw-group-access
* @method bool publishable() Returns true if the page is publishable by the current user, false if not. #pw-group-access
* @method bool listable() Returns true if the page is listable by the current user, false if not. #pw-group-access
* @method bool deleteable() Returns true if the page is deleteable by the current user, false if not. #pw-group-access
* @method bool deletable() Alias of deleteable(). #pw-group-access
* @method bool trashable() Returns true if the page is trashable by the current user, false if not. #pw-group-access
* @method bool trashable($orDeleteable = false) Returns true if the page is trashable by the current user, false if not. #pw-group-access
* @method bool addable($pageToAdd = null) Returns true if the current user can add children to the page, false if not. Optionally specify the page to be added for additional access checking. #pw-group-access
* @method bool moveable($newParent = null) Returns true if the current user can move this page. Optionally specify the new parent to check if the page is moveable to that parent. #pw-group-access
* @method bool sortable() Returns true if the current user can change the sort order of the current page (within the same parent). #pw-group-access
* @property bool $viewable #pw-group-access
* @property bool $editable #pw-group-access
* @property bool $publishable #pw-group-access
* @property bool $deleteable #pw-group-access
* @property bool $deletable #pw-group-access
* @property bool $trashable #pw-group-access
* @property bool $addable #pw-group-access
* @property bool $moveable #pw-group-access
* @property bool $sortable #pw-group-access
* @property bool $listable #pw-group-access
*
* Methods added by LanguageSupport.module (not installed by default)
* -----------------------------------------------------------------
@@ -94,7 +113,7 @@
*
* Methods added by LanguageSupportPageNames.module (not installed by default)
* ---------------------------------------------------------------------------
* @method string localName($language = null) Return the page name in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages
* @method string localName($language = null, $useDefaultWhenEmpty = false) Return the page name in the current users language, or specify $language argument (Language object, name, or ID), or TRUE to use default page name when blank (instead of 2nd argument). #pw-group-languages
* @method string localPath($language = null) Return the page path in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages
* @method string localUrl($language = null) Return the page URL in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages
* @method string localHttpUrl($language = null) Return the page URL (including scheme and hostname) in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages
@@ -127,6 +146,8 @@ class Page extends WireData implements \Countable, WireMatchable {
* never exceeds 1024, otherwise issues in Pages::find() will need to be considered.
*
* The status levels 16384 and above can safely be changed as needed as they are runtime only.
*
* Please note that statuses 2, 32, 256, and 4096 are reserved for future use.
*
*/
@@ -272,7 +293,15 @@ class Page extends WireData implements \Countable, WireMatchable {
* @var Page|null
*
*/
protected $parent = null;
protected $_parent = null;
/**
* Parent ID for lazy loading purposes
*
* @var int
*
*/
protected $_parent_id = 0;
/**
* The previous parent used by the page, if it was changed during runtime.
@@ -329,6 +358,16 @@ class Page extends WireData implements \Countable, WireMatchable {
*/
protected $fieldDataQueue = array();
/**
* Field names that should wakeup and sanitize on first access (populated when isLoaded==false)
*
* These are most likely field names designated as autoload for this page.
*
* @var array of (field name => raw field value)
*
*/
protected $wakeupNameQueue = array();
/**
* Is this a new page (not yet existing in the database)?
*
@@ -362,6 +401,14 @@ class Page extends WireData implements \Countable, WireMatchable {
*/
protected $lazyLoad = false;
/**
* Whether or not pages loaded by this one are allowed to be cached by PagesLoaderCache class
*
* @var bool
*
*/
protected $loaderCache = true;
/**
* Is this page allowing it's output to be formatted?
*
@@ -495,12 +542,16 @@ class Page extends WireData implements \Countable, WireMatchable {
*/
static $baseProperties = array(
'accessTemplate' => 'getAccessTemplate',
'addable' => 'm',
'child' => 'm',
'children' => 'm',
'created' => 's',
'createdStr' => '',
'createdUser' => '',
'created_users_id' => 's',
'deletable' => 'm',
'deleteable' => 'm',
'editable' => 'm',
'editUrl' => 'm',
'fieldgroup' => '',
'filesManager' => 'm',
@@ -517,10 +568,12 @@ class Page extends WireData implements \Countable, WireMatchable {
'isPublic' => 'm',
'isTrash' => 'm',
'isUnpublished' => 'm',
'listable' => 'm',
'modified' => 's',
'modifiedStr' => '',
'modifiedUser' => '',
'modified_users_id' => 's',
'moveable' => 'm',
'name' => 's',
'namePrevious' => 'p',
'next' => 'm',
@@ -533,12 +586,14 @@ class Page extends WireData implements \Countable, WireMatchable {
'parents' => 'm',
'path' => 'm',
'prev' => 'm',
'publishable' => 'm',
'published' => 's',
'publishedStr' => '',
'render' => '',
'rootParent' => 'm',
'siblings' => 'm',
'sort' => 's',
'sortable' => 'm',
'sortfield' => 's',
'status' => 's',
'statusPrevious' => 'p',
@@ -546,7 +601,9 @@ class Page extends WireData implements \Countable, WireMatchable {
'template' => 'p',
'templates_id' => '',
'templatePrevious' => 'p',
'trashable' => 'm',
'url' => 'm',
'viewable' => 'm'
);
/**
@@ -630,8 +687,7 @@ class Page extends WireData implements \Countable, WireMatchable {
$value2 = clone $value;
$this->set($name, $value2); // commit cloned value
// if value is Pagefiles, then tell it the new page
if($value2 instanceof Pagefiles) $value2->setPage($this);
if($value2 instanceof PageFieldValueInterface) $value2->setPage($this);
}
$this->instanceID .= ".clone";
if($track) $this->setTrackChanges(true);
@@ -656,7 +712,7 @@ class Page extends WireData implements \Countable, WireMatchable {
*
* @param string $key Name of property to set
* @param mixed $value Value to set
* @return Page Reference to this Page
* @return Page|WireData Reference to this Page
* @see __set
* @throws WireException
*
@@ -693,9 +749,18 @@ class Page extends WireData implements \Countable, WireMatchable {
break;
case 'parent':
case 'parent_id':
if(($key == 'parent_id' || is_int($value)) && $value) $value = $this->wire('pages')->get((int)$value);
else if(is_string($value)) $value = $this->wire('pages')->get($value);
if($value) $this->setParent($value);
if(is_object($value) && $value instanceof Page) {
// ok
$this->setParent($value);
} else if($value && !$this->_parent &&
($key == 'parent_id' || is_int($value) || (is_string($value) && ctype_digit("$value")))) {
// store only parent ID so that parent is lazy loaded,
// but only if parent hasn't already been previously loaded
$this->_parent_id = (int) $value;
} else if($value && (is_string($value) || is_int($value))) {
$value = $this->_pages('get', $value);
$this->setParent($value);
}
break;
case 'parentPrevious':
if(is_null($value) || $value instanceof Page) $this->parentPrevious = $value;
@@ -743,6 +808,9 @@ class Page extends WireData implements \Countable, WireMatchable {
$this->instanceID = $value;
self::$instanceIDs[$value] = $this->settings['id'];
break;
case 'loaderCache':
$this->loaderCache = (bool) $value;
break;
default:
if(strpos($key, 'name') === 0 && ctype_digit(substr($key, 5)) && $this->wire('languages')) {
// i.e. name1234
@@ -783,9 +851,9 @@ class Page extends WireData implements \Countable, WireMatchable {
*
* #pw-internal
*
* @param string $key
* @param mixed $value
* @return $this
* @param string $key Name of field/property to set
* @param mixed $value Value to set
* @return Page|WireData Returns reference to this page
*
*/
public function setForced($key, $value) {
@@ -804,7 +872,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* @param string $key
* @param mixed $value
* @param bool $load Should the existing value be loaded for change comparisons? (applicable only to non-autoload fields)
* @return $this
* @return Page|WireData Returns reference to this Page
* @throws WireException
*
*/
@@ -841,10 +909,8 @@ class Page extends WireData implements \Countable, WireMatchable {
// if the page is currently loading from the database, we assume that any set values are 'raw' and need to be woken up
if(!$this->isLoaded) {
// send the value to the Fieldtype to be woken up for storage in the page
$value = $field->type->wakeupValue($this, $field, $value);
// queue for wakeup and sanitize on first field access
$this->wakeupNameQueue[$key] = $key;
// page is currently loading, so we don't need to continue any further
return parent::set($key, $value);
}
@@ -853,17 +919,37 @@ class Page extends WireData implements \Countable, WireMatchable {
if(is_null(parent::get($key))) {
// this field is not currently loaded. if the $load param is true, then ...
// retrieve old value first in case it's not autojoined so that change comparisons and save's work
if($load && $this->isLoaded) $this->get($key);
if($load) $this->get($key);
} else if(isset($this->wakeupNameQueue[$key])) {
// autoload value: we don't yet have a "woke" value suitable for change detection, so let it wakeup
if($this->trackChanges() && $load) {
// if changes are being tracked, load existing value for comparison
$this->getFieldValue($key);
} else {
// if changes aren't being tracked, the existing value can be discarded
unset($this->wakeupNameQueue[$key]);
}
} else if($this->outputFormatting && $field->type->formatValue($this, $field, $value) != $value) {
// The field has been loaded or dereferenced from the API, and this field changes when formatters are applied to it.
// There is a good chance they are trying to set a formatted value, and we don't allow this situation because the
// possibility of data corruption is high. We set the Page::statusCorrupted status so that Pages::save() can abort.
$this->set('status', $this->status | self::statusCorrupted);
$corruptedFields = $this->get('_statusCorruptedFields');
if(!is_array($corruptedFields)) $corruptedFields = array();
$corruptedFields[$field->name] = $field->name;
$this->set('_statusCorruptedFields', $corruptedFields);
} else {
// check if the field is corrupted
$isCorrupted = false;
if(is_object($value) && $value instanceof PageFieldValueInterface) {
if($value->formatted()) $isCorrupted = true;
} else if($this->outputFormatting) {
$result = $field->type->_callHookMethod('formatValue', array($this, $field, $value));
if($result != $value) $isCorrupted = true;
}
if($isCorrupted) {
// The field has been loaded or dereferenced from the API, and this field changes when formatters are applied to it.
// There is a good chance they are trying to set a formatted value, and we don't allow this situation because the
// possibility of data corruption is high. We set the Page::statusCorrupted status so that Pages::save() can abort.
$this->set('status', $this->status | self::statusCorrupted);
$corruptedFields = $this->get('_statusCorruptedFields');
if(!is_array($corruptedFields)) $corruptedFields = array();
$corruptedFields[$field->name] = $field->name;
$this->set('_statusCorruptedFields', $corruptedFields);
}
}
// isLoaded so sanitizeValue can determine if it can perform a typecast rather than a full sanitization (when helpful)
@@ -939,8 +1025,12 @@ class Page extends WireData implements \Countable, WireMatchable {
}
switch($key) {
case 'parent':
$value = $this->_parent ? $this->_parent : $this->parent();
break;
case 'parent_id':
$value = $this->parent ? $this->parent->id : 0;
$value = $this->_parent ? $this->_parent->id : 0;
if(!$value) $value = $this->_parent_id;
break;
case 'templates_id':
$value = $this->template ? $this->template->id : 0;
@@ -980,6 +1070,9 @@ class Page extends WireData implements \Countable, WireMatchable {
$value = $this->wire('modules')->get('PageRender');
$value->setPropertyPage($this);
break;
case 'loaderCache':
$value = $this->loaderCache;
break;
default:
if($key && isset($this->settings[(string)$key])) return $this->settings[$key];
@@ -1029,7 +1122,7 @@ class Page extends WireData implements \Countable, WireMatchable {
*/
public function getField($field) {
$template = $this->template;
$fieldgroup = $template ? $this->template->fieldgroup : null;
$fieldgroup = $template ? $template->fieldgroup : null;
if(!$fieldgroup) return null;
if($this->outputFormatting && $fieldgroup->hasFieldContext($field)) {
$value = $fieldgroup->getFieldContext($field);
@@ -1200,7 +1293,8 @@ class Page extends WireData implements \Countable, WireMatchable {
* Get the value for a non-native page field, and call upon Fieldtype to join it if not autojoined
*
* @param string $key Name of field to get
* @param string $selector Optional selector to filter load by
* @param string $selector Optional selector to filter load by...
* ...or, if not in selector format, it becomes an __invoke() argument for object values .
* @return null|mixed
*
*/
@@ -1210,7 +1304,18 @@ class Page extends WireData implements \Countable, WireMatchable {
$field = $this->getField($key);
$value = parent::get($key);
if(!$field) return $value; // likely a runtime field, not part of our data
$invokeArgument = '';
if($value !== null && isset($this->wakeupNameQueue[$key])) {
$value = $field->type->_callHookMethod('wakeupValue', array($this, $field, $value));
$value = $field->type->sanitizeValue($this, $field, $value);
$trackChanges = $this->trackChanges(true);
$this->setTrackChanges(false);
parent::set($key, $value);
$this->setTrackChanges($trackChanges);
unset($this->wakeupNameQueue[$key]);
}
if($field->useRoles && $this->outputFormatting) {
// API access may be limited when output formatting is ON
if($field->flags & Field::flagAccessAPI) {
@@ -1223,29 +1328,38 @@ class Page extends WireData implements \Countable, WireMatchable {
// note: we do not store this blank value in the Page, so that
// the real value can potentially be loaded later without output formatting
$value = $field->type->getBlankValue($this, $field);
return $field->type->formatValue($this, $field, $value);
return $this->formatFieldValue($field, $value);
}
}
if(!is_null($value) && empty($selector)) {
// if the non-filtered value is already loaded, return it
return $this->outputFormatting ? $field->type->formatValue($this, $field, $value) : $value;
return $this->formatFieldValue($field, $value);
}
$track = $this->trackChanges();
$this->setTrackChanges(false);
if(!$field->type) return null;
if($selector && !Selectors::stringHasSelector($selector)) {
// if selector argument provdied, but isn't valid, we assume it
// to instead be an argument for the value's __invoke() method
$invokeArgument = $selector;
$selector = '';
}
if($selector) {
$value = $field->type->loadPageFieldFilter($this, $field, $selector);
} else {
$value = $field->type->loadPageField($this, $field);
// $value = $field->type->loadPageField($this, $field);
$value = $field->type->_callHookMethod('loadPageField', array($this, $field));
}
if(is_null($value)) {
$value = $field->type->getDefaultValue($this, $field);
} else {
$value = $field->type->wakeupValue($this, $field, $value);
$value = $field->type->_callHookMethod('wakeupValue', array($this, $field, $value));
//$value = $field->type->wakeupValue($this, $field, $value);
}
// turn off output formatting and set the field value, which may apply additional changes
@@ -1261,8 +1375,48 @@ class Page extends WireData implements \Countable, WireMatchable {
if(is_object($value) && $value instanceof Wire) $value->resetTrackChanges(true);
if($track) $this->setTrackChanges(true);
$value = $this->formatFieldValue($field, $value);
return $this->outputFormatting ? $field->type->formatValue($this, $field, $value) : $value;
if($invokeArgument && is_object($value) && method_exists($value, '__invoke')) {
$value = $value->__invoke($invokeArgument);
}
return $value;
}
/**
* Return a value consistent with the pages output formatting state
*
* This is primarily for use as a helper to the getFieldValue() method.
*
* @param Field $field
* @param mixed $value
* @return mixed
*
*/
protected function formatFieldValue(Field $field, $value) {
$hasInterface = is_object($value) && $value instanceof PageFieldValueInterface;
if($hasInterface) {
$value->setPage($this);
$value->setField($field);
}
if($this->outputFormatting) {
// output formatting is enabled so return a formatted value
//$value = $field->type->formatValue($this, $field, $value);
$value = $field->type->_callHookMethod('formatValue', array($this, $field, $value));
// check again for interface since value may now be different
if($hasInterface) $hasInterface = is_object($value) && $value instanceof PageFieldValueInterface;
if($hasInterface) $value->formatted(true);
} else if($hasInterface && $value->formatted()) {
// unformatted requested, and value is already formatted so load a fresh copy
$this->__unset($field->name);
$value = $this->getFieldValue($field->name);
}
return $value;
}
/**
@@ -1324,7 +1478,9 @@ class Page extends WireData implements \Countable, WireMatchable {
if($field && count($parts) < 2) {
// this is a field that will provide its own formatted value
$subname = count($parts) == 1 ? array_shift($parts) : '';
if(!$this->wire($subname)) $value = $field->type->markupValue($this, $field, $value, $subname);
if(!$subname || !$this->wire($subname)) {
$value = $field->type->markupValue($this, $field, $value, $subname);
}
}
} while(is_object($value) && count($parts));
@@ -1678,20 +1834,22 @@ class Page extends WireData implements \Countable, WireMatchable {
*
*/
public function setParent(Page $parent) {
if($this->parent && $this->parent->id == $parent->id) return $this;
if($this->_parent && $this->_parent->id == $parent->id) return $this;
if($parent->id && $this->id == $parent->id || $parent->parents->has($this)) {
throw new WireException("Page cannot be its own parent");
}
if($this->isLoaded) {
$this->trackChange('parent', $this->parent, $parent);
if(($this->parent && $this->parent->id) && $this->parent->id != $parent->id) {
if(!$this->_parent) $this->parent(); // force it to load
$this->trackChange('parent', $this->_parent, $parent);
if(($this->_parent && $this->_parent->id) && $this->_parent->id != $parent->id) {
if($this->settings['status'] & Page::statusSystem) {
throw new WireException("Parent changes are disallowed on this page");
}
if(is_null($this->parentPrevious)) $this->parentPrevious = $this->parent;
if(is_null($this->parentPrevious)) $this->parentPrevious = $this->_parent;
}
}
$this->parent = $parent;
$this->_parent = $parent;
$this->_parent_id = $parent->id;
return $this;
}
@@ -1762,7 +1920,7 @@ class Page extends WireData implements \Countable, WireMatchable {
} else if(is_array($selector)) {
$selector["has_parent"] = $this->id;
}
return $this->wire('pages')->find($selector, $options);
return $this->_pages('find', $selector, $options);
}
/**
@@ -1787,7 +1945,7 @@ class Page extends WireData implements \Countable, WireMatchable {
*
* @param string $selector Selector to use, or omit to return all children.
* @param array $options Optional options to modify behavior, the same as those provided to Pages::find.
* @return PageArray Children that matched the selector, or all children (if no selector given)
* @return PageArray|array Returns PageArray for most cases. Returns regular PHP array if using the findIDs option.
* @see Page::child(), Page::find(), Page::numChildren(), Page::hasChildren()
*
*/
@@ -1870,7 +2028,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* #pw-group-common
* #pw-group-traversal
*
* @param string|array $selector Selector to use, or blank to return the first child.
* @param string|array|int $selector Selector to use, or blank to return the first child.
* @param array $options Optional options per Pages::find
* @return Page|NullPage
* @see Page::children()
@@ -1903,10 +2061,16 @@ class Page extends WireData implements \Countable, WireMatchable {
*
*/
public function parent($selector = '') {
if(!$this->parent) return $this->wire('pages')->newNullPage();
if(empty($selector)) return $this->parent;
if($this->parent->matches($selector)) return $this->parent;
if($this->parent->parent_id) return $this->parent->parent($selector); // recursive, in a way
if(!$this->_parent) {
if($this->_parent_id) {
$this->_parent = $this->_pages('get', (int) $this->_parent_id);
} else {
return $this->wire('pages')->newNullPage();
}
}
if(empty($selector)) return $this->_parent;
if($this->_parent->matches($selector)) return $this->_parent;
if($this->_parent->parent_id) return $this->_parent->parent($selector); // recursive, in a way
return $this->wire('pages')->newNullPage();
}
@@ -2235,6 +2399,9 @@ class Page extends WireData implements \Countable, WireMatchable {
if(!is_null($field)) {
if($this->hasField($field)) {
return $this->wire('pages')->saveField($this, $field, $options);
} else if(is_string($field) && (isset($this->settings[$field]) || parent::get($field) !== null)) {
$options['noFields'] = true;
return $this->wire('pages')->save($this, $options);
} else {
return false;
}
@@ -2271,7 +2438,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* ~~~~~
*
* #pw-group-manipulation
* #pw-links [Blog post about setAndSave](https://processwire.com/blog/posts/processwire-2.6.9-core-updates-and-new-procache-version/#new-page-gt-setandsave-method)
* #pw-links [Blog post about setAndSave](https://processwire.com/blog/posts/processwire-2.6.9-core-updates-and-new-procache-version/)
*
* @param array|string $key Field or property name to set, or array of one or more ['property' => $value].
* @param string|int|bool|object $value Value to set, or omit if you provided an array in first argument.
@@ -2360,7 +2527,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* making the return value the same as the `Page::hasChildren()` method.
*
* - When output formatting is off, returns number of all children without exclusion,
* making the return value teh same as the `Page::numChildren()` method.
* making the return value the same as the `Page::numChildren()` method.
*
* ~~~~~
* // Get number of visible children, like $page->hasChildren()
@@ -2453,17 +2620,16 @@ class Page extends WireData implements \Countable, WireMatchable {
if(parent::isChanged($what)) return true;
$changed = false;
if($what) {
$value = $this->get($what);
if(is_object($value) && $value instanceof Wire)
$changed = $value->isChanged();
$data = array_key_exists($what, $this->data) ? array($this->data[$what]) : array();
} else {
foreach($this->data as $key => $value) {
if(is_object($value) && $value instanceof Wire)
$changed = $value->isChanged();
if($changed) break;
}
$data = &$this->data;
}
foreach($data as $key => $value) {
if(is_object($value) && $value instanceof Wire) {
$changed = $value->isChanged();
}
if($changed) break;
}
return $changed;
}
@@ -2543,7 +2709,7 @@ class Page extends WireData implements \Countable, WireMatchable {
}
/**
* Returns the URL to the page
* Returns the URL to the page (optionally with additional $options)
*
* - This method can also be accessed by property `$page->url` (without parenthesis).
*
@@ -2555,7 +2721,34 @@ class Page extends WireData implements \Countable, WireMatchable {
* - **Need to hook this method?** While it's not directly hookable, it does use the `$page->path()`
* method, which *is* hookable. As a result, you can affect the output of the url() method by
* hooking the path() method instead.
*
*
* ## $options argument
*
* You can specify an `$options` argument to this method with any of the following:
*
* - `pageNum` (int|string): Specify pagination number, or "+" for next pagination, or "-" for previous pagination.
* - `urlSegmentStr` (string): Specify a URL segment string to append.
* - `urlSegments` (array): Specify array of URL segments to append (may be used instead of urlSegmentStr).
* - `data` (array): Array of key=value variables to form a query string.
* - `http` (bool): Specify true to make URL include scheme and hostname (default=false).
* - `language` (Language): Specify Language object to return URL in that Language.
*
* You can also specify any of the following for `$options` as shortcuts:
*
* - If you specify an `int` for options it is assumed to be the `pageNum` option.
* - If you specify `+` or `-` for options it is assumed to be the `pageNum` “next/previous pagination” option.
* - If you specify any other `string` for options it is assumed to be the `urlSegmentStr` option.
* - If you specify a `boolean` (true) for options it is assumed to be the `http` option.
*
* Please also note regarding `$options`:
*
* - This method honors template slash settings for page, URL segments and page numbers.
* - Any passed in URL segments are automatically sanitized with `Sanitizer::pageNameUTF8()`.
* - If using the `pageNum` or URL segment options please also make sure these are enabled on the pages template.
* - The query string generated by any `data` variables is entity encoded when output formatting is on.
* - The `language` option requires that the `LanguageSupportPageNames` module is installed.
* - The prefix for page numbers honors `$config->pageNumUrlPrefix` and multi-language prefixes as well.
*
* ~~~~~
* // Using $page->url to output navigation
* foreach($page->children as $child) {
@@ -2567,13 +2760,54 @@ class Page extends WireData implements \Countable, WireMatchable {
* echo $page->url(); // outputs: /my-site/about/contact/
* echo $page->path(); // outputs: /about/contact/
* ~~~~~
* ~~~~~
* // Specify that you want a specific pagination (output: /example/page2)
* echo $page->url(2);
*
* // Get URL for next and previous pagination
* echo $page->url('+'); // next
* echo $page->url('-'); // prev
*
* // Get a URL with scheme and hostname (output: http://domain.com/example/)
* echo $page->url(true);
*
* // Specify a URL segment string (output: /example/photos/1)
* echo $page->url('photos/1');
*
* // Use a URL segment array (output: /example/photos/1)
* echo $page->url([
* 'urlSegments' => [ 'photos', '1' ]
* ]);
*
* // Get URL in a specific language
* $fr = $languages->get('fr');
* echo $page->url($fr);
*
* // Include data/query vars (output: /example/?action=view&type=photos)
* echo $page->url([
* 'data' => [
* 'action' => 'view',
* 'type' => 'photos'
* ]
* ]);
*
* // Specify multiple options (output: http://domain.com/example/foo/page3?bar=baz)
* echo $page->url([
* 'http' => true,
* 'pageNum' => 3,
* 'urlSegmentStr' => 'foo',
* 'data' => [ 'bar' => 'baz' ]
* ]);
* ~~~~~
*
* @param array|int|string|bool|Language|null $options Optionally specify options to modify default behavior (see method description).
* @return string Returns page URL, for example: `/my-site/about/contact/`
* @see Page::path(), Page::httpUrl(), Page::editUrl(), Page::localUrl()
*
*/
public function url() {
$url = rtrim($this->wire('config')->urls->root, "/") . $this->path();
public function url($options = null) {
if($options !== null) return $this->traversal()->urlOptions($this, $options);
$url = rtrim($this->wire('config')->urls->root, "/") . $this->path();
if($this->template->slashUrls === 0 && $this->settings['id'] > 1) $url = rtrim($url, '/');
return $url;
}
@@ -2593,27 +2827,29 @@ class Page extends WireData implements \Countable, WireMatchable {
* // Generating a link to this page using httpUrl
* echo "<a href='$page->httpUrl'>$page->title</a>";
* ~~~~~
*
*
* @param array $options For details on usage see `Page::url()` options argument.
* @return string Returns full URL to page, for example: `https://processwire.com/about/`
* @see Page::url(), Page::localHttpUrl()
*
*/
public function httpUrl() {
public function httpUrl($options = array()) {
if(!$this->template) return '';
switch($this->template->https) {
case -1: $protocol = 'http'; break;
case 1: $protocol = 'https'; break;
default: $protocol = $this->wire('config')->https ? 'https' : 'http';
default: $protocol = $this->wire('config')->https ? 'https' : 'http';
}
return "$protocol://" . $this->wire('config')->httpHost . $this->url();
if(is_array($options)) unset($options['http']);
else if(is_bool($options)) $options = array();
return "$protocol://" . $this->wire('config')->httpHost . $this->url($options);
}
/**
* Return the URL necessary to edit this page
* Return the URL necessary to edit this page
*
* - We recommend checking that the page is editable before outputting the editUrl().
* - If user opens URL in their browser and is not logged in, they must login to account with edit permission.
* - This method can also be accessed by property at `$page->editUrl` (without parenthesis).
*
* ~~~~~~
@@ -2624,14 +2860,23 @@ class Page extends WireData implements \Countable, WireMatchable {
*
* #pw-group-advanced
*
* @param array|bool $options Specify boolean true to force URL to include scheme and hostname, or use $options array:
* - `http` (bool): True to force scheme and hostname in URL (default=auto detect).
* @return string URL for editing this page
*
*/
public function editUrl() {
public function editUrl($options = array()) {
$adminTemplate = $this->wire('templates')->get('admin');
$https = $adminTemplate && ($adminTemplate->https > 0);
$url = ($https && !$this->wire('config')->https) ? 'https://' . $this->wire('config')->httpHost : '';
$url .= $this->wire('config')->urls->admin . "page/edit/?id=$this->id";
if($options === true || (is_array($options) && !empty($options['http']))) {
if(strpos($url, '://') === false) {
$url = ($https ? 'https://' : 'http://') . $this->wire('config')->httpHost . $url;
}
}
$append = $this->wire('session')->getFor($this, 'appendEditUrl');
if($append) $url .= $append;
return $url;
}
@@ -3099,7 +3344,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* - `true` (boolean): To return an array of status names (indexed by status number).
* - `integer|string|array`: Status number(s) or status name(s) to set the current page status (same as $page->status = $value)
* @param int|null $status If you specified `true` for first argument, optionally specify status value you want to use (if not the current).
* @return int|array|$this If setting status, `$this` is returned. If getting status: current status or array of status names is returned.
* @return int|array|Page If setting status, `$this` is returned. If getting status: current status or array of status names is returned.
* @see Page::addStauts(), Page::removeStatus(), Page::hasStatus()
*
*/
@@ -3150,7 +3395,10 @@ class Page extends WireData implements \Countable, WireMatchable {
unset(Page::$loadingStack[$this->settings['id']]);
}
$this->isLoaded = $isLoaded ? true : false;
if($isLoaded) $this->loaded();
if($isLoaded) {
//$this->loaded();
$this->_callHookMethod('loaded');
}
return $this;
}
@@ -3299,6 +3547,7 @@ class Page extends WireData implements \Countable, WireMatchable {
if($value != null && is_object($value)) {
if(method_exists($value, 'uncache') && $value !== $this) $value->uncache();
parent::set($field->name, null);
if(isset($this->wakeupNameQueue[$field->name])) unset($this->wakeupNameQueue[$field->name]);
}
}
}
@@ -3593,5 +3842,43 @@ class Page extends WireData implements \Countable, WireMatchable {
throw new WireException("Invalid arguments to Page::lazy()");
}
}
/**
* Handles get/find loads specific to this Page from the $pages API variable
*
* #pw-internal
*
* @param string $method The $pages API method to call (get, find, findOne, or count)
* @param string|int $selector The selector argument of the $pages call
* @param array $options Any additional options (see Pages::find for options).
* @return Pages|Page|PageArray|NullPage
* @throws WireException
*
*/
public function _pages($method = '', $selector = '', $options = array()) {
if(empty($method)) return $this->wire('pages');
if(!isset($options['cache'])) $options['cache'] = $this->loaderCache;
if(!isset($options['caller'])) $options['caller'] = "page._pages.$method";
$result = $this->wire('pages')->$method($selector, $options);
return $result;
}
/*
public function remove($key) {
parent::remove($key);
if(isset($this->data[$key])) {
$a = parent::get('_statusCorruptedFields');
if(!is_array($a)) $a = array();
$k = array_search($key, $a);
if($k !== false) {
unset($a[$k]);
if(empty($a)) $this->removeStatus(self::statusCorrupted);
parent::set('_statusCorruptedFields', $a);
}
}
return $this;
}
*/
}

View File

@@ -11,7 +11,13 @@
*/
class PageAccess {
/**
* @var ProcessWire
*
*/
protected $wire;
/**
* Allowed types for page access
*
@@ -43,7 +49,7 @@ class PageAccess {
$permission = $this->wire('permissions')->get($name);
$name = $permission ? $permission->name : 'edit';
} else if($name instanceof Permission) {
$name = $permission->name;
$name = $name->name;
}
if(strpos($name, 'page-') === 0) $name = str_replace('page-', '', $name);
@@ -56,12 +62,13 @@ class PageAccess {
* Returns the parent page that has the template from which we get our role/access settings from
*
* @param Page $page
* @param string Type, one of 'view', 'edit', 'create' or 'add' (default='view')
* @param string $type Type, one of 'view', 'edit', 'create' or 'add' (default='view')
* @param int $level Recursion level for internal use only
* @return Page|NullPage Returns NullPage if none found
*
*/
public function getAccessParent(Page $page, $type = 'view', $level = 0) {
if(!$page->id) return $page->wire('pages')->newNullPage();
if(!in_array($type, $this->types)) $type = $this->getType($type);
if($page->template->useRoles || $page->id === 1) {
// found an access parent
@@ -80,7 +87,7 @@ class PageAccess {
* Returns the template from which we get our role/access settings from
*
* @param Page $page
* @param string Type, one of 'view', 'edit', 'create' or 'add' (default='view')
* @param string $type Type, one of 'view', 'edit', 'create' or 'add' (default='view')
* @return Template|null Returns null if none
*
*/
@@ -114,7 +121,7 @@ class PageAccess {
*
* @param Page $page
* @param string|int|Role $role
* @param string Default is 'view', but you may specify 'create' or 'add' as well
* @param string $type Default is 'view', but you may specify 'create' or 'add' as well
* @return bool
*
*/
@@ -125,4 +132,43 @@ class PageAccess {
if(is_int($role)) return $roles->has("id=$role");
return false;
}
/**
* Get or inject a ProcessWire API variable or fuel a new object instance
*
* See Wire::wire() for explanation of all options.
*
* @param string|WireFuelable $name Name of API variable to retrieve, set, or omit to retrieve entire Fuel object.
* @param null|mixed $value Value to set if using this as a setter, otherwise omit.
* @param bool $lock When using as a setter, specify true if you want to lock the value from future changes (default=false)
* @return mixed|Fuel
* @throws WireException
*
*/
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);
return $this->wire;
}
/**
* Set the ProcessWire instance
*
* @param ProcessWire $wire
*
*/
public function setWire(ProcessWire $wire) {
$this->wire = $wire;
}
/**
* Get the ProcessWire instance
*
* @return ProcessWire
*
*/
public function getWire() {
return $this->wire;
}
}

View File

@@ -14,13 +14,19 @@
*
* PageArray is returned by all API methods in ProcessWire that can return more than one page at once.
* `$pages->find()` and `$page->children()` are common examples.
*
* The recommended way to create a new PageArray is to use the `$pages->newPageArray()` method:
* ~~~~~
* $pageArray = $pages->newPageArray();
* ~~~~~
* #pw-body
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* https://processwire.com
*
* @method string getMarkup($key = null) Render a simple/default markup value for each item #pw-internal
*
* @property Page|null $first First item
* @property Page|null $last Last item
* @property Page[] $data #pw-internal
*
*/
@@ -278,7 +284,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
* #pw-internal
*
* @param int|Page $key
* @return bool true if removed, false if not
* @return $this This PageArray instance
*
*/
public function remove($key) {
@@ -345,7 +351,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
* #pw-internal
*
* @param int $num Number of items to return
* @return PageArray
* @return PageArray|WireArray New PageArray instance
*
*/
public function findRandom($num) {
@@ -362,7 +368,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
* @param int $start Starting index.
* @param int $limit Number of items to include. If omitted, includes the rest of the array.
* @return PageArray
* @return PageArray|WireArray New PageArray instance
*
*/
public function slice($start, $limit = 0) {
@@ -377,7 +383,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
* #pw-internal
*
* @param int $num Return the nth item in this WireArray. Specify a negative number to count from the end rather than the start.
* @return Page|null
* @return Page|Wire|null Returns Page object or null if not present
*
*/
public function eq($num) {
@@ -447,7 +453,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
* @param string|Selectors|array $selectors AttributeSelector string to use as the filter.
* @param bool $not Make this a "not" filter? (default is false)
* @return PageArray reference to current [filtered] instance
* @return PageArray|WireArray reference to current [filtered] PageArray
*
*/
protected function filterData($selectors, $not = false) {
@@ -461,7 +467,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
* #pw-internal
*
* @param string $selector AttributeSelector string to use as the filter.
* @return PageArray reference to current instance.
* @return PageArray|PaginatedArray|WireArray reference to current PageArray instance.
*
*/
public function filter($selector) {
@@ -474,7 +480,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
* #pw-internal
*
* @param string $selector AttributeSelector string to use as the filter.
* @return PageArray reference to current instance.
* @return PageArray|PaginatedArray|WireArray reference to current PageArray instance.
*
*/
public function not($selector) {
@@ -489,7 +495,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
* #pw-internal
*
* @param string $selector AttributeSelector string.
* @return PageArray
* @return PageArray|WireArray New PageArray instance
*
*/
public function find($selector) {
@@ -561,7 +567,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
* #pw-internal
*
* @return Page[]|\ArrayObject
* @return Page[]|\ArrayObject|PageArrayIterator
*
*/
public function getIterator() {
@@ -622,7 +628,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
public function __debugInfo() {
$info = parent::__debugInfo();
$info['selectors'] = (string) $this->selectors;
if(!count($info['selectors'])) unset($info['selectors']);
if(!wireCount($info['selectors'])) unset($info['selectors']);
return $info;
}

View File

@@ -73,7 +73,7 @@ class PageArrayIterator extends Wire implements \Iterator {
* @var int
*
*/
protected $chunkSize = 1000;
protected $chunkSize = 250;
/**
* Construct
@@ -92,7 +92,7 @@ class PageArrayIterator extends Wire implements \Iterator {
*
*/
protected function loadChunk() {
$this->chunkSize = (int) $this->wire('config')->lazyPageChunkSize;
$this->pagesPosition = 0;
$start = $this->currentChunk++ * $this->chunkSize;
@@ -121,10 +121,11 @@ class PageArrayIterator extends Wire implements \Iterator {
$ids[] = $page->id;
}
$debug = $this->wire('pages');
if($debug) $this->wire('pages')->debug(false);
$this->pages = $this->wire('pages')->getById($ids, $options);
if($debug) $this->wire('pages')->debug(true);
$pages = $this->wire('pages');
$debug = $pages->debug();
if($debug) $pages->debug(false);
$this->pages = $pages->getById($ids, $options);
if($debug) $pages->debug(true);
}
$this->pagesCount = count($this->pages);

View File

@@ -1,163 +0,0 @@
<?php namespace ProcessWire;
/**
* Class PageExport
*
* PLEASE NOTE: this class is not yet functional and here as a work in progress, not currently used by the core.
*
* @todo make this module use a 'guid', adding it if not there already
*
*/
class PageExport extends Wire {
/**
* Export the page's data to an array that can be later imported
*
* @param Page $page
* @return array
*
*/
public function export(Page $page) {
$of = $page->of();
$page->of(false);
// todo: make user definable guid, except for ID part
$guid = $this->wire('config')->httpHost . $this->wire('config')->urls->root . $page->id;
$data = array(
'id' => $page->id,
'guid' => $guid,
'parent_id' => $page->parent_id,
'parent' => $page->parent->path,
'templates_id' => $page->templates_id,
'template' => $page->template->name,
'name' => $page->name,
'status' => $page->status,
'sort' => $page->sort,
'sortfield' => $page->sortfield,
'num_children' => $page->numChildren(),
'created' => $page->created,
'created_users_id' => $page->created_users_id,
'created_user' => $page->createdUser->name,
'modified' => $page->modified,
'modified_users_id' => $page->modified_users_id,
'modified_user' => $page->modifiedUser->name,
'published' => $page->published,
'core_version' => $this->wire('config')->version,
'export_time' => time(),
'data' => array(),
'types' => array(),
);
foreach($page->template->fieldgroup as $field) {
if($field->type instanceof FieldtypeFieldsetOpen) continue;
$data['data'][$field->name] = $this->exportValue($page, $field, $page->get($field->name));
$data['types'][$field->name] = $field->type->className();
}
$page->of($of);
return $data;
}
public function ___import($page, $data = null) {
if(is_null($data)) {
$data = $page;
$page = $this->wire('pages')->newPage();
}
if(empty($data['core_version'])) throw new WireException("Invalid import data");
$page->of(false);
$page->resetTrackChanges(true);
if(!is_array($data)) throw new WireException("Data passed to import() must be an array");
if(!$page->parent_id) {
$parent = $this->wire('pages')->get($data['parent']);
if(!$parent->id) throw new WireException("Unknown parent: $data[parent]");
$page->parent = $parent;
}
if(!$page->templates_id) {
$template = $this->wire('templates')->get($data['template']);
if(!$template) throw new WireException("Unknown template: $data[template]");
$page->template = $template;
}
$page->name = $data['name'];
$page->sort = $data['sort'];
$page->sortfield = $data['sortfield'];
$page->status = $data['status'];
$page->guid = $data['id'];
if(!$page->id) $page->save();
foreach($data['data'] as $name => $value) {
$field = $this->wire('fields')->get($name);
if(!$field) {
$this->error("Unknown field: $name");
continue;
}
if($data['types'][$name] != $field->type->className()) {
$this->error("Import data for field '$field->name' has different fieldtype '" . $data['types'][$name] . "' != '" . $field->type->className() . "', skipping...");
continue;
}
$newStr = var_export($value, true);
$oldStr = var_export($this->exportValue($page, $field, $page->get($field->name)), true);
if($newStr === $oldStr) continue; // value has not changed, so abort
$value = $this->importValue($page, $field, $value);
$page->set($field->name, $value);
}
return $page;
}
protected function exportValue($page, $field, $value) {
return $field->type->exportValue($page, $field, $value);
/*
$sleepValue = $field->type->sleepValue($page, $field, $value);
if($field->type instanceof FieldtypePage) {
foreach($sleepValue as $key => $id) {
$p = $this->wire('pages')->get($id);
$info = array('id' => $p->id, 'path' => $p->path);
$sleepValue[$key] = $info;
}
}
return $sleepValue;
*/
}
protected function importValue($page, $field, $value) {
return $field->type->importValue($page, $field, $value);
/*
if($field->type instanceof FieldtypePage) {
foreach($value as $key => $info) {
// convert $value[$key] from array to page ID
$p = $this->wire('pages')->get((int) $info['id']);
if(!$p->id || $p->path != $info['path']) {
// if page ID wasn't found or path doesn't match, then we try to retrieve it by path instead
// since path may be a more reliable indicator
$p2 = $this->wire('pages')->get($info['path']);
if($p2->id) $p = $p2;
}
$value[$key] = $p->id;
}
}
$value = $field->type->wakeupValue($page, $field, $value);
return $value;
*/
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -34,15 +34,17 @@ class PageTraversal {
// onlyVisible takes the place of selector
$onlyVisible = $selector;
if(!$onlyVisible) return $page->get('numChildren');
return $page->wire('pages')->count("parent_id=$page->id");
return $page->_pages('count', "parent_id=$page->id");
} else if($selector === 1) {
// viewable pages only
$numChildren = $page->get('numChildren');
if(!$numChildren) return 0;
if($page->wire('user')->isSuperuser()) return $numChildren;
if($page->wire('user')->hasPermission('page-edit')) return $page->wire('pages')->count("parent_id=$page->id, include=unpublished");
return $page->wire('pages')->count("parent_id=$page->id, include=hidden");
if($page->wire('user')->hasPermission('page-edit')) {
return $page->_pages('count', "parent_id=$page->id, include=unpublished");
}
return $page->_pages('count', "parent_id=$page->id, include=hidden");
} else if(empty($selector) || (!is_string($selector) && !is_array($selector))) {
return $page->get('numChildren');
@@ -53,7 +55,7 @@ class PageTraversal {
} else if(is_array($selector)) {
$selector["parent_id"] = $page->id;
}
return $page->wire('pages')->count($selector);
return $page->_pages('count', $selector);
}
}
@@ -63,11 +65,11 @@ class PageTraversal {
* @param Page $page
* @param string|array $selector Selector to use, or blank to return all children
* @param array $options
* @return PageArray
* @return PageArray|array
*
*/
public function children(Page $page, $selector = '', $options = array()) {
if(!$page->numChildren) return $page->wire('pages')->newPageArray();
if(!$page->numChildren) return $page->_pages()->newPageArray();
$defaults = array('caller' => 'page.children');
$options = array_merge($defaults, $options);
$sortfield = $page->sortfield();
@@ -81,7 +83,7 @@ class PageTraversal {
$selector = trim("parent_id=$page->id, $selector", ", ");
if(strpos($selector, 'sort=') === false) $selector .= ", sort=$sortfield";
}
return $page->wire('pages')->find($selector, $options);
return $page->_pages('find', $selector, $options);
}
/**
@@ -96,7 +98,7 @@ class PageTraversal {
*
*/
public function child(Page $page, $selector = '', $options = array()) {
if(!$page->numChildren) return $page->wire('pages')->newNullPage();
if(!$page->numChildren) return $page->_pages()->newNullPage();
$defaults = array('getTotal' => false, 'caller' => 'page.child');
$options = array_merge($defaults, $options);
if(is_array($selector)) {
@@ -107,7 +109,7 @@ class PageTraversal {
if(strpos($selector, 'start=') === false) $selector .= ", start=0"; // prevent pagination
}
$children = $this->children($page, $selector, $options);
return count($children) ? $children->first() : $page->wire('pages')->newNullPage();
return count($children) ? $children->first() : $page->_pages()->newNullPage();
}
/**
@@ -211,7 +213,7 @@ class PageTraversal {
$selector = trim($selector, ", ");
}
$options = array('caller' => 'page.siblings');
return $page->wire('pages')->find($selector, $options);
return $page->_pages('find', $selector, $options);
}
/**
@@ -247,18 +249,18 @@ class PageTraversal {
} else if(strpos($until, '/') === 0) {
// page path
$stopPage = $pages->get($until);
$stopPage = $page->_pages('get', $until);
} else if(is_array($selector) || is_array($options['until'])) {
// either selector or until is an array
$s = new Selectors($options['until']);
foreach(new Selectors($selector) as $item) $s->add($item);
$s->add(new SelectorEqual('limit', 1));
$stopPage = $page->wire('pages')->find($s)->first();
$stopPage = $page->_pages('find', $s)->first();
} else {
// selector string
$stopPage = $page->wire('pages')->find("$selector, limit=1, $until")->first();
$stopPage = $page->_pages('find', "$selector, limit=1, $until")->first();
}
if($stopPage && $stopPage->id) {
@@ -324,6 +326,7 @@ class PageTraversal {
} else if($options['all']) {
$result = $pages->getById($rows, array(
'parent_id' => $parent->id,
'cache' => $page->loaderCache
));
if($options['all'] && $options['prev']) $result = $result->reverse();
@@ -332,7 +335,8 @@ class PageTraversal {
$result = $pages->getById(array($row['id']), array(
'template' => $page->wire('templates')->get($row['templates_id']),
'parent_id' => $row['parent_id'],
'getOne' => true
'getOne' => true,
'cache' => $page->loaderCache
));
}
@@ -447,6 +451,132 @@ class PageTraversal {
$options = array_merge($options, $defaults);
return $this->_next($page, $filter, $options);
}
/**
* Returns the URL to the page with $options
*
* You can specify an `$options` argument to this method with any of the following:
*
* - `pageNum` (int|string): Specify pagination number, or "+" for next pagination, or "-" for previous pagination.
* - `urlSegmentStr` (string): Specify a URL segment string to append.
* - `urlSegments` (array): Specify array of URL segments to append (may be used instead of urlSegmentStr).
* - `data` (array): Array of key=value variables to form a query string.
* - `http` (bool): Specify true to make URL include scheme and hostname (default=false).
* - `language` (Language): Specify Language object to return URL in that Language.
*
* You can also specify any of the following for `$options` as shortcuts:
*
* - If you specify an `int` for options it is assumed to be the `pageNum` option.
* - If you specify `+` or `-` for options it is assumed to be the `pageNum` “next/previous pagination” option.
* - If you specify any other `string` for options it is assumed to be the `urlSegmentStr` option.
* - If you specify a `boolean` (true) for options it is assumed to be the `http` option.
*
* Please also note regarding `$options`:
*
* - This method honors template slash settings for page, URL segments and page numbers.
* - Any passed in URL segments are automatically sanitized with `Sanitizer::pageNameUTF8()`.
* - If using the `pageNum` or URL segment options please also make sure these are enabled on the pages template.
* - The query string generated by any `data` variables is entity encoded when output formatting is on.
* - The `language` option requires that the `LanguageSupportPageNames` module is installed.
* - The prefix for page numbers honors `$config->pageNumUrlPrefix` and multi-language prefixes as well.
*
* @param Page $page
* @param array|int|string|bool|Language $options Optionally specify options to modify default behavior (see method description).
* @return string Returns page URL, for example: `/my-site/about/contact/`
* @see Page::path(), Page::httpUrl(), Page::editUrl(), Page::localUrl()
*
*/
public function urlOptions(Page $page, $options = array()) {
$config = $page->wire('config');
$template = $page->template;
$defaults = array(
'http' => is_bool($options) ? $options : false,
'pageNum' => is_int($options) || (is_string($options) && in_array($options, array('+', '-'))) ? $options : 1,
'data' => array(),
'urlSegmentStr' => is_string($options) ? $options : '',
'urlSegments' => array(),
'language' => is_object($options) && $options instanceof Page && $options->className() === 'Language' ? $options : null,
);
if(empty($options)) {
$url = rtrim($config->urls->root, '/') . $page->path();
if($template->slashUrls === 0 && $page->id > 1) $url = rtrim($url, '/');
return $url;
}
$options = is_array($options) ? array_merge($defaults, $options) : $defaults;
$sanitizer = $page->wire('sanitizer');
$language = null;
$url = null;
if(count($options['urlSegments'])) {
$options['urlSegmentStr'] = implode('/', $options['urlSegments']);
}
if($options['language'] && $page->wire('modules')->isInstalled('LanguageSupportPageNames')) {
if(!is_object($options['language'])) {
$options['language'] = null;
} else if(!$options['language'] instanceof Page) {
$options['language'] = null;
} else if(strpos($options['language']->className(), 'Language') === false) {
$options['language'] = null;
}
if($options['language']) {
/** @var Language $language */
$language = $options['language'];
// localUrl method provided as hook by LanguageSupportPageNames
$url = $page->localUrl($language);
}
}
if(is_null($url)) {
$url = rtrim($config->urls->root, '/') . $page->path();
if($template->slashUrls === 0 && $page->id > 1) $url = rtrim($url, '/');
}
if(is_string($options['urlSegmentStr']) && strlen($options['urlSegmentStr'])) {
$url = rtrim($url, '/') . '/' . $sanitizer->pagePathNameUTF8(trim($options['urlSegmentStr'], '/'));
if($template->slashUrlSegments === '' || $template->slashUrlSegments) $url .= '/';
}
if($options['pageNum']) {
if($options['pageNum'] === '+') {
$options['pageNum'] = $page->wire('input')->pageNum + 1;
} else if($options['pageNum'] === '-' || $options['pageNum'] === -1) {
$options['pageNum'] = $page->wire('input')->pageNum - 1;
}
if((int) $options['pageNum'] > 1) {
$prefix = '';
if($language) {
$lsp = $page->wire('modules')->get('LanguageSupportPageNames');
$prefix = $lsp ? $lsp->get("pageNumUrlPrefix$language") : '';
}
if(!strlen($prefix)) $prefix = $config->pageNumUrlPrefix;
$url = rtrim($url, '/') . '/' . $prefix . ((int) $options['pageNum']);
if($template->slashPageNum) $url .= '/';
}
}
if(count($options['data'])) {
$query = http_build_query($options['data']);
if($page->of()) $query = $sanitizer->entities($query);
$url .= '?' . $query;
}
if($options['http']) {
switch($template->https) {
case -1: $scheme = 'http'; break;
case 1: $scheme = 'https'; break;
default: $scheme = $config->https ? 'https' : 'http';
}
$url = "$scheme://" . $page->wire('config')->httpHost . $url;
}
return $url;
}
/******************************************************************************************************************
* LEGACY METHODS

View File

@@ -7,6 +7,7 @@
* #pw-summary-traversal For the most part youll want to traverse from the parent `Pagefiles` object than these methods.
* #pw-summary-manipulation Remember to follow up any manipulations with a `$pages->save()` call.
* #pw-summary-tags Be sure to see the `Pagefiles::getTag()` and `Pagesfiles::findTag()` methods, which enable you retrieve files by tag.
* #pw-use-constructor
* #pw-body =
* Pagefile objects are contained by a `Pagefiles` object.
* #pw-body
@@ -19,7 +20,8 @@
* @property-read string $filename full disk path to the file on the server.
* @property-read string $name Returns the filename without the path, same as the "basename" property.
* @property-read string $hash Get a unique hash (for the page) representing this Pagefile.
* @property-read string $tagsArray Get file tags as an array. #pw-group-tags @since 3.0.17
* @property-read string $tagsArray Get file tags as an array. #pw-group-tags @since 3.0.17
* @property int $sort Sort order in database. #pw-group-other
* @property string $basename Returns the filename without the path.
* @property string $description Value of the files description field (string), if enabled. Note you can also set this property directly.
* @property string $tags Value of the files tags field (string), if enabled. #pw-group-tags
@@ -48,9 +50,19 @@ class Pagefile extends WireData {
/**
* Reference to the owning collection of Pagefiles
*
* @var Pagefiles
*
*/
protected $pagefiles;
protected $pagefiles;
/**
* Extra file data
*
* @var array
*
*/
protected $filedata = array();
/**
* Construct a new Pagefile
@@ -114,8 +126,17 @@ class Pagefile extends WireData {
*
*/
protected function ___install($filename) {
$basename = $filename;
if(strpos($basename, '?') !== false) {
list($basename, $queryString) = explode('?', $basename);
if($queryString) {} // do not use in basename
}
if(empty($basename)) throw new WireException("Empty filename");
$basename = $this->pagefiles->cleanBasename($filename, true, false, true);
$basename = $this->pagefiles->cleanBasename($basename, true, false, true);
$pathInfo = pathinfo($basename);
$basename = basename($basename, ".$pathInfo[extension]");
@@ -155,19 +176,25 @@ class Pagefile extends WireData {
*
* @param string $key
* @param mixed $value
* @return $this
* @return Pagefile|WireData
*
*/
public function set($key, $value) {
if($key == 'basename') $value = $this->pagefiles->cleanBasename($value, false);
if($key == 'description') return $this->setDescription($value);
if($key == 'modified') $value = ctype_digit("$value") ? (int) $value : strtotime($value);
if($key == 'created') $value = ctype_digit("$value") ? (int) $value : strtotime($value);
if($key == 'tags') {
if($key == 'basename') {
$value = $this->pagefiles->cleanBasename($value, false);
} else if($key == 'description') {
return $this->setDescription($value);
} else if($key == 'modified') {
$value = ctype_digit("$value") ? (int) $value : strtotime($value);
} else if($key == 'created') {
$value = ctype_digit("$value") ? (int) $value : strtotime($value);
} else if($key == 'tags') {
$this->tags($value);
return $this;
} else if($key == 'filedata') {
if(is_array($value)) $this->filedata($value);
return $this;
}
if(strpos($key, 'description') === 0 && preg_match('/^description(\d+)$/', $value, $matches)) {
@@ -182,16 +209,75 @@ class Pagefile extends WireData {
return parent::set($key, $value);
}
/**
* Get or set filedata
*
* Filedata is any additional data that you want to store with the files database record.
*
*
* - To get a value, specify just the $key argument. Null is returned if request value is not present.
* - To get all values, omit all arguments. An associative array will be returned.
* - To set a value, specify the $key and the $value to set.
* - To set all values at once, specify an associative array for the $key argument.
* - To unset, specify boolean false (or null) for $key, and the name of the property to unset as $value.
* - To unset, you can also get all values, unset it from the retuned array, and set the array back.
*
* #pw-internal
*
* @param string|array|false|null $key Specify array to set all file data, or key (string) to set or get a property,
* Or specify boolean false to remove key specified by $value argument.
* @param null|string|array|int|float $value Specify a value to set for given property
* @return Pagefile|Pageimage|array|string|int|float|bool|null
*
*/
public function filedata($key = '', $value = null) {
$filedata = $this->filedata;
$changed = false;
if($key === false || $key === null) {
// unset property named in $value
if(!empty($value) && isset($filedata[$value])) {
unset($this->filedata[$value]);
$changed = true;
}
} else if(empty($key)) {
// return all
return $filedata;
} else if(is_array($key)) {
// set all
if($key != $filedata) {
$this->filedata = $key;
$changed = true;
}
} else if($value === null) {
// return value for key
return isset($this->filedata[$key]) ? $this->filedata[$key] : null;
} else {
// set value for key
if(!isset($filedata[$key]) || $filedata[$key] != $value) {
$this->filedata[$key] = $value;
$changed = true;
}
}
if($changed) {
$this->trackChange('filedata', $filedata, $this->filedata);
if($this->page && $this->field) $this->page->trackChange($this->field->name);
}
return $this;
}
/**
* Set a description, optionally parsing JSON language-specific descriptions to separate properties
*
* @param string $value
* @param string|array $value
* @param Page|Language Langage to set it for. Omit to determine automatically.
* @return $this
*
*/
protected function setDescription($value, Page $language = null) {
/** @var Languages $languages */
$languages = $this->wire('languages');
/** @var Language|null $language */
$field = $this->field;
@@ -203,27 +289,46 @@ class Pagefile extends WireData {
$name .= $language->id;
}
parent::set($name, $value);
if($name != 'description' && $this->isChanged($name)) $this->trackChange('description');
return $this;
}
// check if it contains JSON?
$first = substr($value, 0, 1);
$last = substr($value, -1);
if(($first == '{' && $last == '}') || ($first == '[' && $last == ']')) {
$values = json_decode($value, true);
if(is_array($value)) {
$values = $value;
} else {
$values = array();
// check if it contains JSON?
$first = substr($value, 0, 1);
$last = substr($value, -1);
if(($first == '{' && $last == '}') || ($first == '[' && $last == ']')) {
$values = json_decode($value, true);
} else {
$values = array();
}
}
$numChanges = 0;
if($values && count($values)) {
$n = 0;
foreach($values as $id => $v) {
// first item is always default language. this ensures description will still
// work even if language support is later uninstalled.
if($noLang && $n > 0) continue;
$name = $n > 0 ? "description$id" : "description";
parent::set($name, $v);
$name = 'description';
if($noLang && $n > 0) break;
$n++;
if(ctype_digit("$id")) {
$id = (int) $id;
if(!$id) $id = '';
$name = $n > 0 ? "description$id" : "description";
} else if($id === 'default') {
$name = 'description';
} else if($languages) {
$language = $languages->get($id); // i.e. "default" or "es"
if(!$language->id) continue;
$name = $language->isDefault() ? "description" : "description$language->id";
}
parent::set($name, $v);
if($this->isChanged($name)) $numChanges++;
}
} else {
// no JSON values so assume regular language description
@@ -231,11 +336,15 @@ class Pagefile extends WireData {
$language = $languages ? $this->wire('user')->language : null;
if($languages && $language && !$noLang && !$language->isDefault()) {
parent::set("description$language", $value);
$name = "description$language->id";
} else {
parent::set("description", $value);
$name = "description";
}
parent::set($name, $value);
if($this->isChanged($name)) $numChanges++;
}
if($numChanges && !$this->isChanged('description')) $this->trackChange('description');
return $this;
}
@@ -264,17 +373,31 @@ class Pagefile extends WireData {
* #pw-group-common
* #pw-group-manipulation
*
* @param null|bool|Language
* @param null|bool|Language|array
* - To GET in current user language: Omit arguments or specify null.
* - To GET in another language: Specify a Language name, id or object.
* - To GET in all languages as a JSON string: Specify boolean true (if LanguageSupport not installed, regular string returned).
* - To GET in all languages as an array indexed by language name: Specify boolean true for both arguments.
* - To SET for a language: Specify a language name, id or object, plus the $value as the 2nd argument.
* - To SET in all languages as a JSON string: Specify boolean true, plus the JSON string $value as the 2nd argument (internal use only).
* - To SET in all languages as an array: Specify the array here, indexed by language ID or name, and omit 2nd argument.
* @param null|string $value Specify only when you are setting (single language) rather than getting a value.
* @return string
*
*/
public function description($language = null, $value = null) {
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) {
$value[$language->name] = (string) parent::get("description" . ($language->isDefault() ? '' : $language->id));
}
return $value;
}
if(!is_null($value)) {
// set description mode
@@ -287,6 +410,13 @@ class Pagefile extends WireData {
}
return $value;
}
if(is_array($language)) {
// set all from array, then return description in current language
$this->setDescription($language);
$language = null;
$value = null;
}
if((is_string($language) || is_int($language)) && $this->wire('languages')) {
// convert named or ID'd languages to Language object
@@ -367,7 +497,7 @@ class Pagefile extends WireData {
break;
case 'URL':
// nocache url
$value = $this->url() . '?nc=' . @filemtime($this->filename());
$value = $this->noCacheURL();
break;
case 'pagefiles':
$value = $this->pagefiles;
@@ -386,6 +516,10 @@ class Pagefile extends WireData {
parent::set($key, $value);
}
break;
case 'fileData':
case 'filedata':
$value = $this->filedata();
break;
case 'mtime':
$value = filemtime($this->filename());
break;
@@ -394,6 +528,18 @@ class Pagefile extends WireData {
return $value;
}
/**
* Hookable no-cache URL
*
* #pw-internal
*
* @return string
*
*/
protected function ___noCacheURL() {
return $this->url() . '?nc=' . @filemtime($this->filename());
}
/**
* Return the next sibling Pagefile in the parent Pagefiles, or NULL if at the end.
*
@@ -759,7 +905,7 @@ class Pagefile extends WireData {
*
*/
public function __toString() {
return $this->basename;
return (string) $this->basename;
}
/**
@@ -816,7 +962,7 @@ class Pagefile extends WireData {
* #pw-internal
*
* @param string $path Path (not including basename)
* @return mixed result of copy() function
* @return bool result of copy() function
*
*/
public function copyToPath($path) {

View File

@@ -47,10 +47,11 @@
* @property Page $page Returns the Page that contains this set of files, same as the getPage() method. #pw-group-other
* @property Field $field Returns the Field that contains this set of files, same as the getField() method. #pw-group-other
* @method Pagefiles delete() delete(Pagefile $file) Removes the file and deletes from disk when page is saved. #pw-group-manipulation
* @method Pagefile|bool clone(Pagefile $item, array $options = array()) Duplicate a file and return it. #pw-group-manipulation
*
*/
class Pagefiles extends WireArray {
class Pagefiles extends WireArray implements PageFieldValueInterface {
/**
* The Page object associated with these Pagefiles
@@ -75,6 +76,22 @@ class Pagefiles extends WireArray {
*
*/
protected $unlinkQueue = array();
/**
* Items to be renamed when Page is saved (oldName => newName)
*
* @var array
*
*/
protected $renameQueue = array();
/**
* Items to be made non-temp upon page save (like duplicated files)
*
* @var array
*
*/
protected $unTempQueue = array();
/**
* IDs of any hooks added in this instance, used by the destructor
@@ -83,6 +100,14 @@ class Pagefiles extends WireArray {
*
*/
protected $hookIDs = array();
/**
* Whether or not this is a formatted value
*
* @var bool
*
*/
protected $formatted = false;
/**
* Construct a Pagefiles object
@@ -161,7 +186,7 @@ class Pagefiles extends WireArray {
*
* #pw-internal
*
* @return WireArray
* @return Pagefiles|WireArray
*
*/
public function makeNew() {
@@ -310,10 +335,13 @@ class Pagefiles extends WireArray {
public function add($item) {
if(is_string($item)) {
/** @var Pagefile $item */
$item = $this->wire(new Pagefile($this, $item));
}
return parent::add($item);
/** @var Pagefiles $result */
$result = parent::add($item);
return $result;
}
/**
@@ -323,14 +351,36 @@ class Pagefiles extends WireArray {
*
*/
public function hookPageSave() {
if($this->page && $this->field && !$this->page->isChanged($this->field->name)) return $this;
foreach($this->unTempQueue as $item) {
$item->isTemp(false);
}
foreach($this->unlinkQueue as $item) {
$item->unlink();
}
foreach($this->renameQueue as $item) {
$name = $item->get('_rename');
if(!$name) continue;
$item->rename($name);
}
$this->unTempQueue = array();
$this->unlinkQueue = array();
$this->renameQueue = array();
$this->removeHooks();
return $this;
}
protected function addSaveHook() {
if(!count($this->unlinkQueue) && !count($this->renameQueue) && !count($this->unTempQueue)) {
$this->hookIDs[] = $this->page->filesManager->addHookBefore('save', $this, 'hookPageSave');
}
}
/**
* Delete a pagefile item
@@ -364,9 +414,7 @@ class Pagefiles extends WireArray {
public function remove($item) {
if(is_string($item)) $item = $this->get($item);
if(!$this->isValidItem($item)) throw new WireException("Invalid type to {$this->className}::remove(item)");
if(!count($this->unlinkQueue)) {
$this->hookIDs[] = $this->page->filesManager->addHookBefore('save', $this, 'hookPageSave');
}
$this->addSaveHook();
$this->unlinkQueue[] = $item;
parent::remove($item);
return $this;
@@ -390,6 +438,86 @@ class Pagefiles extends WireArray {
return $this;
}
/**
* Queue a rename of a Pagefile
*
* This only queues a rename. Rename actually occurs when page is saved.
* Note this differs from the behavior of `Pagefile::rename()`.
*
* #pw-group-manipulation
*
* @param Pagefile $item
* @param string $name
* @return Pagefiles
* @see Pagefile::rename()
*
*/
public function rename(Pagefile $item, $name) {
$item->set('_rename', $name);
$this->renameQueue[] = $item;
$this->trackChange('renameQueue', $item->name, $name);
$this->addSaveHook();
return $this;
}
/**
* Duplicate the Pagefile and add to this Pagefiles instance
*
* After duplicating a file, you must follow up with a save of the page containing it.
* Otherwise the file is marked for deletion.
*
* @param Pagefile $item Pagefile item to duplicate
* @param array $options Options to modify default behavior:
* - `action` (string): Specify "append", "prepend", "after", "before" or blank to only return Pagefile. (default="after")
* - `pagefiles` (Pagefiles): Pagefiles instance file should be duplicated to. (default=$this)
* @return Pagefile|bool Returns new Pagefile or boolean false on fail
*
*/
public function ___clone(Pagefile $item, array $options = array()) {
$defaults = array(
'action' => 'after',
'pagefiles' => $this,
);
$options = array_merge($defaults, $options);
/** @var Pagefiles $pagefiles */
$pagefiles = $options['pagefiles'];
$itemCopy = false;
$path = $pagefiles->path();
$parts = explode('.', $item->basename(), 2);
$n = $path === $this->path() ? 1 : 0;
if($n && preg_match('/^(.+?)-(\d+)$/', $parts[0], $matches)) {
$parts[0] = $matches[1];
$n = (int) $matches[2];
}
do {
$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);
$itemCopy = clone $item;
$itemCopy->setPagefilesParent($pagefiles);
$itemCopy->setFilename($pathname);
$itemCopy->isTemp(true);
switch($options['action']) {
case 'append': $pagefiles->append($itemCopy); break;
case 'prepend': $pagefiles->prepend($itemCopy); break;
case 'before': $pagefiles->insertBefore($itemCopy, $item); break;
case 'after': $pagefiles->insertAfter($itemCopy, $item); break;
}
$pagefiles->unTempQueue($itemCopy);
}
return $itemCopy;
}
/**
* Return the full disk path where files are stored
*
@@ -510,6 +638,59 @@ class Pagefiles extends WireArray {
}
return $item;
}
/**
* Get list of tags for all files in this Pagefiles array, or return files matching given tag(s)
*
* This method can either return a list of all tags available, or return all files
* matching the given tag or tags (an alias of findTag method).
*
* ~~~~~
* // Get string of all tags
* $tagsString = $page->files->tags();
*
* // Get array of all tags
* $tagsArray = $page->files->tags(true);
*
* // Find all files matching given tag
* $pagefiles = $page->files->tags('foobar');
* ~~~~~
*
* #pw-group-tags
*
* @param bool|string|array $value Specify one of the following:
* - Omit to return all tags as a string.
* - Boolean true if you want to return tags as an array (rather than string).
* - Boolean false to return tags as an array, with lowercase enforced.
* - String if you want to return files matching tags (See `Pagefiles::findTag()` method for usage)
* - Array if you want to return files matching tags (See `Pagefiles::findTag()` method for usage)
* @return string|array|Pagefiles Returns all tags as a string or an array, or Pagefiles matching given tag(s).
* When a tags array is returned, it is an associative array where the key and value are both the tag (keys are always lowercase).
* @see Pagefiles::findTag(), Pagefile::tags()
*
*/
public function tags($value = null) {
if($value === null) {
$returnString = true;
$value = true;
} else {
$returnString = false;
}
if(is_bool($value)) {
// return array of tags
$tags = array();
foreach($this as $pagefile) {
$tags = array_merge($tags, $pagefile->tags($value));
}
if($returnString) $tags = implode(' ', $tags);
return $tags;
}
// fallback to behavior of findTag
return $this->findTag($value);
}
/**
* Track a change
@@ -522,10 +703,11 @@ class Pagefiles extends WireArray {
* @return $this
*
*/
public function trackChange($what, $old = null, $new = null) {
if($this->field && $this->page) $this->page->trackChange($this->field->name);
return parent::trackChange($what, $old, $new);
/** @var Pagefiles $result */
$result = parent::trackChange($what, $old, $new);
return $result;
}
/**
@@ -653,6 +835,19 @@ class Pagefiles extends WireArray {
return count($removed);
}
/**
* Add Pagefile as item to have temporary status removed when Page is saved
*
* #pw-internal
*
* @param Pagefile $pagefile
*
*/
public function unTempQueue(Pagefile $pagefile) {
$this->addSaveHook();
$this->unTempQueue[] = $pagefile;
}
/**
* Is the given Pagefiles identical to this one?
*
@@ -677,13 +872,14 @@ class Pagefiles extends WireArray {
* @return $this
*
*/
public function resetTrackChanges($trackChanges = true) {
$this->unlinkQueue = array();
if($this->page && $this->page->id && $this->field) {
$this->page->untrackChange($this->field->name);
}
return parent::resetTrackChanges($trackChanges);
/** @var Pagefiles $result */
$result = parent::resetTrackChanges($trackChanges);
return $result;
}
/**
@@ -696,6 +892,17 @@ class Pagefiles extends WireArray {
//$this->page = null;
}
/**
* Get or set formatted state
*
* @param bool|null $set
* @return bool
*
*/
public function formatted($set = null) {
if(is_bool($set)) $this->formatted = $set;
return $this->formatted;
}
}

View File

@@ -572,15 +572,23 @@ class PagefilesManager extends Wire {
}
/**
* Return a path where temporary files can be stored.
* Return a path where temporary files can be stored unique to this ProcessWire instance
*
* @return string
*
*/
public function getTempPath() {
static $wtd = null;
if(is_null($wtd)) $wtd = $this->wire(new WireTempDir($this->className() . $this->page->id));
if(is_null($wtd)) {
$wtd = new WireTempDir();
$this->wire($wtd);
$wtd->setMaxAge(3600);
$name = $wtd->createName('PFM');
$wtd->create($name);
}
return $wtd->get();
// if(is_null($wtd)) $wtd = $this->wire(new WireTempDir($this->className() . $this->page->id));
// return $wtd->get();
}
}

View File

@@ -6,6 +6,7 @@
* #pw-summary Represents an image item attached to a page, typically via an Image Fieldtype.
* #pw-summary-variations A variation refers to an image that is based upon another (like a resized or cropped version for example).
* #pw-order-groups common,resize-and-crop,variations,other
* #pw-use-constructor
* #pw-body =
* Pageimage objects are usually contained by a `Pageimages` object, which is a type of `Pagefiles` and `WireArray`.
* In addition to the methods and properties below, you'll also want to look at `Pagefile` which this class inherits
@@ -23,18 +24,20 @@
* ~~~~~
* #pw-body
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
* @property int $width Width of image, in pixels.
* @property int $height Height of image, in pixels.
* @property int $hidpiWidth HiDPI width of image, in pixels. #pw-internal
* @property int $hidpiHeight HiDPI heigh of image, in pixels. #pw-internal
* @property string $error Last image resizing error message, when applicable. #pw-group-resize-and-crop
* @property Pageimage $original Reference to original $image, if this is a resized version. #pw-group-variations
* @property string $url
* @property string $basename
* @property string $filename
* @property-read int $width Width of image, in pixels.
* @property-read int $height Height of image, in pixels.
* @property-read int $hidpiWidth HiDPI width of image, in pixels. #pw-internal
* @property-read int $hidpiHeight HiDPI heigh of image, in pixels. #pw-internal
* @property-read string $error Last image resizing error message, when applicable. #pw-group-resize-and-crop
* @property-read Pageimage $original Reference to original $image, if this is a resized version. #pw-group-variations
* @property-read string $url
* @property-read string $basename
* @property-read string $filename
* @property-read array $focus Focus array contains 'top' (float), 'left' (float), 'zoom' (int), and 'default' (bool) properties.
* @property-read bool $hasFocus Does this image have custom focus settings? (i.e. $focus['default'] == true)
*
* @method bool|array isVariation($basename, $allowSelf = false)
* @method Pageimage crop($x, $y, $width, $height, $options = array())
@@ -93,9 +96,14 @@ class Pageimage extends Pagefile {
protected $error = '';
/**
* Construct a new Pagefile
* Construct a new Pageimage
*
* ~~~~~
* // Construct a new Pageimage, assumes that $page->images is a FieldtypeImage Field
* $pageimage = new Pageimage($page->images, '/path/to/file.png');
* ~~~~~
*
* @param Pagefiles $pagefiles
* @param Pageimages|Pagefiles $pagefiles
* @param string $filename Full path and filename to this pagefile
* @throws WireException
*
@@ -171,6 +179,119 @@ class Pageimage extends Pagefile {
}
}
/**
* Get or set focus area for crops to use
*
* These settings are used by $this->size() calls that specify BOTH width AND height. Focus helps to
* ensure that the important subject of the photo is not cropped out when the requested size proportion
* differs from the original image proportion. For example, not chopping off someones head in a photo.
*
* Default behavior is to return an array containing "top" and "left" indexes, representing percentages
* from top and left. When arguments are specified, you are either setting the top/left percentages, or
* unsetting focus, or getting focus in different ways, described in arguments below.
*
* A zoom argument/property is also present here for future use, but not currently supported.
*
* #pw-group-other
*
* @param null|float|int|array|false $top Omit to get focus array, or specify one of the following:
* - GET: Omit all arguments to get focus array (default behavior).
* - GET: Specify boolean TRUE to return TRUE if focus data is present or FALSE if not.
* - GET: Specify integer 1 to make this method return pixel dimensions rather than percentages.
* - SET: Specify both $top and $left arguments to set (values assumed to be percentages).
* - SET: Specify array containing "top" and "left" indexes to set (percentages).
* - SET: Specify array where index 0 is top and index 1 is left (percentages).
* - SET: Specify string in the format "top left", i.e. "25 70" (percentages).
* - UNSET: Specify boolean false to remove any focus values.
* @param null|float|int $left Set left value (when $top value is float|int)
* - This argument is only used when setting focus and should be omitted otherwise.
* @param null|int $zoom Zoom percent (not currently supported)
* @return array|bool|Pageimage Returns one of the following:
* - When getting returns array containing top, left and default properties.
* - When TRUE was specified for the $top argument, it returns either TRUE (has focus) or FALSE (does not have).
* - When setting or unsetting returns $this.
*
*/
public function focus($top = null, $left = null, $zoom = null) {
if(is_string($top) && strpos($top, ' ') && $left === null) {
// SET string like "25 70 0" (representing "top left zoom")
if(strpos($top, ' ') != strrpos($top, ' ')) {
// with zoom
list($top, $left, $zoom) = explode(' ', $top, 3);
} else {
// without zoom
list($top, $left) = explode(' ', $top, 2);
$zoom = 0;
}
}
if($top === null || $top === true || ($top === 1 && $left === null)) {
// GET
$focus = $this->filedata('focus');
if(!is_array($focus) || empty($focus)) {
// use default
if($top === true) return false;
$focus = array(
'top' => 50,
'left' => 50,
'zoom' => 0,
'default' => true,
'str' => '50 50 0',
);
} else {
// use custom
if($top === true) return true;
if(!isset($focus['zoom'])) $focus['zoom'] = 0;
$focus['default'] = false;
$focus['str'] = "$focus[top] $focus[left] $focus[zoom]";
}
if($top === 1) {
// return pixel dimensions rather than percentages
$centerX = ($focus['left'] / 100) * $this->width(); // i.e. (50 / 100) * 500 = 250;
$centerY = ($focus['top'] / 100) * $this->height();
$focus['left'] = $centerX;
$focus['top'] = $centerY;
}
return $focus;
} else if($top === false) {
// UNSET
$this->filedata(false, 'focus');
} else if($top !== null && $left !== null) {
// SET
if(is_array($top)) {
if(isset($top['left'])) {
$left = $top['left'];
$top = $top['top'];
$zoom = isset($top['zoom']) ? $top['zoom'] : 0;
} else {
$top = $top[0];
$left = $top[1];
$zoom = isset($top[2]) ? $top[2] : 0;
}
}
$top = (float) $top;
$left = (float) $left;
$zoom = (int) $zoom;
if(((int) $top) == 50 && ((int) $left) == 50 && ($zoom < 2)) {
// if matches defaults, then no reason to store in filedata
$this->filedata(false, 'focus');
} else {
$this->filedata('focus', array(
'top' => round($top, 1),
'left' => round($left, 1),
'zoom' => $zoom
));
}
}
return $this;
}
/**
* Get a property from this Pageimage
*
@@ -200,6 +321,12 @@ class Pageimage extends Pagefile {
case 'error':
$value = $this->error;
break;
case 'focus':
$value = $this->focus();
break;
case 'hasFocus':
$value = $this->focus(true);
break;
default:
$value = parent::get($key);
}
@@ -211,7 +338,8 @@ class Pageimage extends Pagefile {
*
* #pw-internal
*
* @param bool $reset
* @param bool|string $reset Specify true to retrieve info fresh, or filename to check and return info for.
* When specifying a filename, the info is only returned (not populated with this object).
* @return array
*
*/
@@ -220,25 +348,73 @@ class Pageimage extends Pagefile {
if($reset) $checkImage = true;
else if($this->imageInfo['width']) $checkImage = false;
else $checkImage = true;
$imageInfo = $this->imageInfo;
$filename = is_string($reset) && file_exists($reset) ? $reset : '';
if($checkImage) {
if($checkImage || $filename) {
if($this->ext == 'svg') {
$xml = @file_get_contents($this->filename);
if($xml) {
$a = @simplexml_load_string($xml)->attributes();
$this->imageInfo['width'] = (int) $a->width > 0 ? (int) $a->width : '100%';
$this->imageInfo['height'] = (int) $a->height > 0 ? (int) $a->height : '100%';
}
$info = $this->getImageInfoSVG($filename);
$imageInfo['width'] = $info['width'];
$imageInfo['height'] = $info['height'];
} else {
$info = @getimagesize($this->filename);
if($filename) {
$info = @getimagesize($filename);
} else {
$info = @getimagesize($this->filename);
}
if($info) {
$this->imageInfo['width'] = $info[0];
$this->imageInfo['height'] = $info[1];
$imageInfo['width'] = $info[0];
$imageInfo['height'] = $info[1];
}
}
if(!$filename) $this->imageInfo = $imageInfo;
}
return $this->imageInfo;
return $imageInfo;
}
/**
* Gets the image info/size of an SVG
*
* Returned width and height values may be integers OR percentage strings.
*
* #pw-internal
*
* @param string $filename Optional filename to check
* @return array of width and height
*
*/
protected function getImageInfoSVG($filename = '') {
$width = 0;
$height = 0;
if(!$filename) $filename = $this->filename;
$xml = @file_get_contents($filename);
if($xml) {
$a = @simplexml_load_string($xml)->attributes();
if((int) $a->width > 0) $width = (int) $a->width;
if((int) $a->height > 0) $height = (int) $a->height;
}
if((!$width || !$height) && (extension_loaded('imagick') || class_exists('\IMagick'))) {
try {
$imagick = new \Imagick();
$imagick->readImage($filename);
$width = $imagick->getImageWidth();
$height = $imagick->getImageHeight();
} catch(\Exception $e) {
// fallback to 100%
}
}
if($width < 1) $width = '100%';
if($height < 1) $height = '100%';
return array(
'width' => $width,
'height' => $height
);
}
/**
@@ -285,7 +461,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): 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=center).
* - `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).
@@ -295,10 +471,16 @@ class Pageimage extends Pagefile {
* - `hidpi` (bool): Use HiDPI/retina pixel doubling? (default=false).
* - `hidpiQuality` (bool): Quality setting for HiDPI (default=40, typically lower than regular quality setting).
* - `cleanFilename` (bool): Clean filename of historical resize information for shorter filenames? (default=false).
* - `nameWidth` (int): Width to use for filename (default is to use specified $width argument).
* - `nameHeight` (int): Height to use for filename (default is to use specified $height argument).
* - `focus` (bool): Should resizes that result in crop use focus area if available? (default=true).
* In order for focus to be applicable, resize must include both width and height.
*
* **Possible values for "cropping" option**
*
* - `center` (string): to crop to center of image, default behavior.
* - `true` (bool): Auto detect and allow use of focus (default).
* - `false` (bool): Disallow cropping.
* - `center` (string): to crop to center of image.
* - `x111y222` (string): to crop by pixels, 111px from left and 222px from top (replacing 111 and 222 with your values).
* - `north` (string): Crop North (top), may also be just "n".
* - `northwest` (string): Crop from Northwest (top left), may also be just "nw".
@@ -309,7 +491,9 @@ class Pageimage extends Pagefile {
* - `west` (string): Crop West (left), may also be just "w".
* - `east` (string): Crop East (right), may alos be just "e".
* - `blank` (string): Specify a blank string to disallow cropping during resize.
*
* - `array(111,222)` (array): Array of integers index 0 is left pixels and index 1 is top pixels.
* - `array('11%','22%')` (array): Array of '%' appended strings where index 0 is left percent and index 1 is top percent.
*
* **Note about "quality" and "upscaling" options**
*
* ProcessWire doesn't keep separate copies of images with different "quality" or "upscaling" values.
@@ -356,16 +540,6 @@ class Pageimage extends Pagefile {
*/
protected function ___size($width, $height, $options) {
// I was getting unnecessarily resized images without this code below,
// but this may be better solved in ImageSizer?
/*
$w = $this->width();
$h = $this->height();
if($w == $width && $h == $height) return $this;
if(!$height && $w == $width) return $this;
if(!$width && $h == $height) return $this;
*/
if($this->ext == 'svg') return $this;
if(!is_array($options)) {
@@ -392,6 +566,8 @@ class Pageimage extends Pagefile {
$defaultOptions = array(
'upscaling' => true,
'cropping' => true,
'interlace' => false,
'sharpening' => 'soft',
'quality' => 90,
'hidpiQuality' => 40,
'suffix' => array(), // can be array of suffixes or string of 1 suffix
@@ -400,6 +576,10 @@ class Pageimage extends Pagefile {
'cleanFilename' => false, // clean filename of historial resize information
'rotate' => 0,
'flip' => '',
'nameWidth' => null, // override width to use for filename, int when populated
'nameHeight' => null, // override height to use for filename, int when populated
'focus' => true, // allow single dimension resizes to use focus area?
'zoom' => null, // zoom override, used only if focus is applicable, int when populated
);
$this->error = '';
@@ -407,16 +587,25 @@ class Pageimage extends Pagefile {
$configOptions = $this->wire('config')->imageSizerOptions;
if(!is_array($configOptions)) $configOptions = array();
$options = array_merge($defaultOptions, $configOptions, $options);
if($options['cropping'] === 1) $options['cropping'] = true;
$width = (int) $width;
$height = (int) $height;
if(is_string($options['cropping'])
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
$options['cropping'] = array("$focus[left]%", "$focus[top]%", "$focus[zoom]");
$crop = ''; // do not add suffix
} else if(is_string($options['cropping'])
&& strpos($options['cropping'], 'x') === 0
&& preg_match('/^x(\d+)[yx](\d+)/', $options['cropping'], $matches)) {
$options['cropping'] = true;
$options['cropExtra'] = array((int) $matches[1], (int) $matches[2], $width, $height);
$crop = '';
} else {
$crop = ImageSizer::croppingValueStr($options['cropping']);
}
@@ -426,9 +615,15 @@ class Pageimage extends Pagefile {
$options['suffix'] = empty($options['suffix']) ? array() : explode(' ', $options['suffix']);
}
if($options['rotate'] && !in_array(abs((int) $options['rotate']), array(90, 180, 270))) $options['rotate'] = 0;
if($options['rotate']) $options['suffix'][] = ($options['rotate'] > 0 ? "rot" : "tor") . abs($options['rotate']);
if($options['flip']) $options['suffix'][] = strtolower(substr($options['flip'], 0, 1)) == 'v' ? 'flipv' : 'fliph';
if($options['rotate'] && !in_array(abs((int) $options['rotate']), array(90, 180, 270))) {
$options['rotate'] = 0;
}
if($options['rotate']) {
$options['suffix'][] = ($options['rotate'] > 0 ? "rot" : "tor") . abs($options['rotate']);
}
if($options['flip']) {
$options['suffix'][] = strtolower(substr($options['flip'], 0, 1)) == 'v' ? 'flipv' : 'fliph';
}
$suffixStr = '';
if(!empty($options['suffix'])) {
@@ -447,20 +642,28 @@ class Pageimage extends Pagefile {
if($options['hidpiQuality']) $options['quality'] = $options['hidpiQuality'];
}
//$basename = $this->pagefiles->cleanBasename($this->basename(), false, false, false);
// cleanBasename($basename, $originalize = false, $allowDots = true, $translate = false)
$originalName = $this->basename();
$basename = basename($originalName, "." . $this->ext()); // i.e. myfile
// determine basename without extension, i.e. myfile
$basename = basename($originalName, "." . $this->ext());
$originalSize = $debug ? @filesize($this->filename) : 0;
if($options['cleanFilename'] && strpos($basename, '.') !== false) {
$basename = substr($basename, 0, strpos($basename, '.'));
}
$basename .= '.' . $width . 'x' . $height . $crop . $suffixStr . "." . $this->ext(); // i.e. myfile.100x100.jpg or myfile.100x100nw-suffix1-suffix2.jpg
// filename uses requested width/height unless another specified via nameWidth or nameHeight options
$nameWidth = is_int($options['nameWidth']) ? $options['nameWidth'] : $width;
$nameHeight = is_int($options['nameHeight']) ? $options['nameHeight'] : $height;
// i.e. myfile.100x100.jpg or myfile.100x100nw-suffix1-suffix2.jpg
$basename .= '.' . $nameWidth . 'x' . $nameHeight . $crop . $suffixStr . "." . $this->ext();
$filenameFinal = $this->pagefiles->path() . $basename;
$filenameUnvalidated = '';
$exists = file_exists($filenameFinal);
// create a new resize if it doesn't already exist or forceNew option is set
if(!$exists || $options['forceNew']) {
// filenameUnvalidated is temporary filename used for resize
$filenameUnvalidated = $this->pagefiles->page->filesManager()->getTempPath() . $basename;
if($exists && $options['forceNew']) @unlink($filenameFinal);
if(file_exists($filenameUnvalidated)) @unlink($filenameUnvalidated);
@@ -786,13 +989,18 @@ class Pageimage extends Pagefile {
*
*/
public function maxSize($width, $height, $options = array()) {
$w = $this->width();
$h = $this->height();
if($w >= $h) {
return $this->maxWidth($width, $options);
} else {
return $this->maxHeight($height, $options);
$options['upscaling'] = false;
$options['cropping'] = false;
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();
$suffix[] = 'ms';
$options['suffix'] = $suffix;
}
return $this->size($width, $height, $options);
}
/**
@@ -872,6 +1080,7 @@ class Pageimage extends Pagefile {
* - `1` (int): Rebuild all non-suffix variations, and those w/suffix specifed in $suffix argument. ($suffix is INCLUSION list)
* - `2` (int): Rebuild all variations, except those with suffix specified in $suffix argument. ($suffix is EXCLUSION list)
* - `3` (int): Rebuild only variations specified in the $suffix argument. ($suffix is ONLY-INCLUSION list)
* - `4` (int): Rebuild only non-proportional, non-crop variations (variations that specify both width and height)
*
* Mode 0 is the only truly safe mode, as in any other mode there are possibilities that the resulting
* rebuild of the variation may not be exactly what was intended. The issues with other modes primarily
@@ -899,7 +1108,7 @@ class Pageimage extends Pagefile {
$options['forceNew'] = true;
foreach($this->getVariations(array('info' => true)) as $info) {
$o = $options;
unset($o['cropping']);
$skip = false;
@@ -943,6 +1152,11 @@ class Pageimage extends Pagefile {
}
}
if($mode == 4 && ($info['width'] == 0 || $info['height'] == 0)) {
// skip images that don't specify both width and height
$skip = true;
}
if($skip) {
$skipped[] = $name;
continue;
@@ -952,13 +1166,35 @@ class Pageimage extends Pagefile {
$o['forceNew'] = true;
$o['suffix'] = $info['suffix'];
if(is_file($info['path'])) unlink($info['path']);
/*
if(!$info['width'] && $info['actualWidth']) {
$info['width'] = $info['actualWidth'];
$options['nameWidth'] = 0;
}
if(!$info['height'] && $info['actualHeight']) {
$info['height'] = $info['actualHeight'];
$options['nameHeight'] = 0;
}
*/
if($info['crop'] && preg_match('/^x(\d+)y(\d+)$/', $info['crop'], $matches)) {
// dimensional cropping info contained in filename
$cropX = (int) $matches[1];
$cropY = (int) $matches[2];
$variation = $this->crop($cropX, $cropY, $info['width'], $info['height'], $options);
$variation = $this->crop($cropX, $cropY, $info['width'], $info['height'], $options);
} else if($info['crop']) {
// direct cropping info contained in filename
$options['cropping'] = $info['crop'];
$variation = $this->size($info['width'], $info['height'], $options);
} else if($this->hasFocus) {
// crop to focus area, which the size() method will determine on its own
$variation = $this->size($info['width'], $info['height'], $options);
} else {
if($info['crop']) $options['cropping'] = $info['crop'];
// no crop, no focus, just resize
$variation = $this->size($info['width'], $info['height'], $options);
}
@@ -986,8 +1222,10 @@ class Pageimage extends Pagefile {
* - `original` (string): Original basename
* - `url` (string): URL to image
* - `path` (string): Full path + filename to image
* - `width` (int): Specified width
* - `height` (int): Specified height
* - `width` (int): Specified width in filename
* - `height` (int): Specified height in filename
* - `actualWidth` (int): Actual width when checked manually
* - `actualHeight` (int): Acual height when checked manually
* - `crop` (string): Cropping info string or blank if none
* - `suffix` (array): Array of suffixes
*
@@ -1111,7 +1349,10 @@ class Pageimage extends Pagefile {
} else {
return false;
}
$actualInfo = $this->getImageInfo($info['path']);
$info['actualWidth'] = $actualInfo['width'];
$info['actualHeight'] = $actualInfo['height'];
$info['hidpiWidth'] = $this->hidpiWidth(0, $info['width']);
$info['hidpiHeight'] = $this->hidpiWidth(0, $info['height']);

View File

@@ -24,17 +24,20 @@
*
* HOOKABLE METHODS
* ================
* @method PageArray find() find($selectorString, array $options = array()) Find and return all pages matching the given selector string. Returns a PageArray. #pw-group-retrieval
* @method bool save() save(Page $page) Save any changes made to the given $page. Same as : $page->save() Returns true on success. #pw-group-manipulation
* @method bool saveField() saveField(Page $page, $field) Save just the named field from $page. Same as: $page->save('field') #pw-group-manipulation
* @method bool trash() 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 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 saveField(Page $page, $field, array $options = array()) Save just the named field from $page. Same as: $page->save('field') #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 emptyTrash() Empty the trash and return number of pages deleted. #pw-group-manipulation
* @method bool delete() delete(Page $page, $recursive = false) Permanently delete a page and it's fields. Unlike trash(), pages deleted here are not restorable. If you attempt to delete a page with children, and don't specifically set the $recursive param to True, then this method will throw an exception. If a recursive delete fails for any reason, an exception will be thrown. #pw-group-manipulation
* @method bool delete(Page $page, $recursive = false, array $options = array()) Permanently delete a page and it's fields. Unlike trash(), pages deleted here are not restorable. If you attempt to delete a page with children, and don't specifically set the $recursive param to True, then this method will throw an exception. If a recursive delete fails for any reason, an exception will be thrown. #pw-group-manipulation
* @method Page|NullPage clone(Page $page, Page $parent = null, $recursive = true, $options = array()) Clone an entire page, it's assets and children and return it. #pw-group-manipulation
* @method Page|NullPage add($template, $parent, $name = '', array $values = array()) #pw-group-manipulation
* @method int sort(Page $page, $value = false) Set the “sort” value for given $page while adjusting siblings, or re-build sort for its children. #pw-group-manipulation
* @method setupNew(Page $page) Setup new page that does not yet exist by populating some fields to it. #pw-internal
* @method string setupPageName(Page $page, array $options = []) 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
*
* METHODS PURELY FOR HOOKS
* ========================
@@ -53,6 +56,7 @@
* @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 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.
* @method statusChanged(Page $page) Hook called after a page status has been changed and saved.
* @method publishReady(Page $page) Hook called just before an unpublished page is published.
@@ -61,8 +65,8 @@
* @method unpublished(Page $page) Hook called after a published page has just been unpublished.
* @method saveFieldReady(Page $page, Field $field) Hook called just before a saveField() method saves a page fied.
* @method savedField(Page $page, Field $field) Hook called after saveField() method successfully executes.
* @method saveEitherReady(Page $page, $fieldName = '') Hook inclusive of both saveReady() and saveFieldReady().
* @method savedEither(Page $page, array $changes) Hook inclusive of both saved() and savedField().
* @method savePageOrFieldReady(Page $page, $fieldName = '') Hook inclusive of both saveReady() and saveFieldReady().
* @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
@@ -138,12 +142,12 @@ class Pages extends Wire {
*/
public function __construct(ProcessWire $wire) {
$this->setWire($wire);
$this->debug = $wire->config->debug === Config::debugVerbose ? true : false;
$this->sortfields = $this->wire(new PagesSortfields());
$this->loader = $this->wire(new PagesLoader($this));
$this->cacher = $this->wire(new PagesLoaderCache($this));
$this->trasher = null;
$this->editor = null;
$this->debug = $wire->config->debug;
}
/**
@@ -199,21 +203,24 @@ class Pages extends Wire {
*
* @param string|int|array|Selectors $selector Specify selector (standard usage), but can also accept page ID or array of page IDs.
* @param array|string $options One or more options that can modify certain behaviors. May be associative array or "key=value" selector string.
* - `findOne` (boolean): Apply optimizations for finding a single page.
* - `findAll` (boolean): Find all pages with no exculsions (same as include=all option).
* - `getTotal` (boolean): Whether to set returning PageArray's "total" property (default: true except when findOne=true).
* - `loadPages` (boolean): Whether to populate the returned PageArray with found pages (default: true).
* - `findOne` (boolean): Apply optimizations for finding a single page (default=false).
* - `findAll` (boolean): Find all pages with no exclusions, same as "include=all" option (default=false).
* - `findIDs` (boolean|int): Specify 1 to return array of only page IDs, or true to return verbose array (default=false).
* - `getTotal` (boolean): Whether to set returning PageArray's "total" property (default=true, except when findOne=true).
* - `loadPages` (boolean): Whether to populate the returned PageArray with found pages (default=true).
* The only reason why you'd want to change this to false would be if you only needed the count details from
* the PageArray: getTotal(), getStart(), getLimit, etc. This is intended as an optimization for $pages->count().
* Does not apply if $selector argument is an array.
* - `caller` (string): Optional name of calling function, for debugging purposes, i.e. pages.count
* - `include` (string): Optional inclusion mode of 'hidden', 'unpublished' or 'all'. Default=none. Typically you would specify this
* - `cache` (boolean): Allow caching of selectors and loaded pages? (default=true). Also sets loadOptions[cache].
* - `allowCustom` (boolean): Allow use of _custom="another selector" in given $selector? For specific uses. (default=false)
* - `caller` (string): Optional name of calling function, for debugging purposes, i.e. "pages.count" (default=blank).
* - `include` (string): Optional inclusion mode of 'hidden', 'unpublished' or 'all'. (default=none). Typically you would specify this
* directly in the selector string, so the option is mainly useful if your first argument is not a string.
* - `stopBeforeID` (int): Stop loading pages once page matching this ID is found (default=0).
* - `startAfterID` (int): Start loading pages once page matching this ID is found (default=0).
* - `lazy` (bool): Specify true to force lazy loading. This is the same as using the Pages::findMany() method (default=false).
* - `loadOptions` (array): Optional assoc array of options to pass to getById() load options.
* @return PageArray Pages that matched the given selector.
* - `loadOptions` (array): Optional associative array of options to pass to getById() load options.
* @return PageArray|array PageArray of that matched the given selector, or array of page IDs (if using findIDs option).
*
* Non-visible pages are excluded unless an "include=x" mode is specified in the selector
* (where "x" is "hidden", "unpublished" or "all"). If "all" is specified, then non-accessible
@@ -252,7 +259,7 @@ class Pages extends Wire {
}
/**
* Like find(), but with “lazy loading” to support giant result sets without running of memory.
* Like find(), but with “lazy loading” to support giant result sets without running out of memory.
*
* When using this method, you can retrieve tens of thousands, or hundreds of thousands of pages
* or more, without needing a pagination "limit" in your selector. Individual pages are loaded
@@ -278,8 +285,8 @@ class Pages extends Wire {
*
* #pw-group-retrieval
*
* @param $selector
* @param array $options
* @param string|array|Selectors $selector Selector to find pages
* @param array $options Options to modify behavior. See `Pages::find()` $options argument for details.
* @return PageArray
* @since 3.0.19
* @see Pages::find(), Pages::findOne()
@@ -289,11 +296,56 @@ class Pages extends Wire {
$debug = $this->debug;
if($debug) $this->debug(false);
$options['lazy'] = true;
$matches = $this->loader->find($selector, $options);
$options['caller'] = 'pages.findMany';
if(!isset($options['cache'])) $options['cache'] = false;
$matches = $this->find($selector, $options);
if($debug) $this->debug($debug);
return $matches;
}
/**
* Like $pages->find() except returns array of IDs rather than Page objects.
*
* - This is a faster method to use when you only need to know the matching page IDs.
* - The default behavior is to simply return a regular PHP array of matching page IDs in order.
* - The alternate behavior (verbose) returns more information for each match, as outlined below.
*
* **Verbose option:**
* When specifying boolean true for the `$options` argument (or using the `verbose` option),
* the return value is an array of associative arrays, with each of those associative arrays
* containing `id`, `parent_id` and `templates_id` keys for each page.
*
* ~~~~~
* // returns array of page IDs (integers) like [ 1234, 1235, 1236 ]
* $a = $pages->findIDs("foo=bar");
*
* // verbose option: returns array of associative arrays, each with id, parent_id and templates_id
* $a = $pages->findIDs("foo=bar", true);
* ~~~~~
*
* #pw-group-retrieval
*
* @param string|array|Selectors $selector Selector to find page IDs.
* @param array|bool $options Options to modify behavior.
* - `verbose` (bool): Specify true to make return value array of associative arrays, each with verbose info.
* - The verbose option above can also be specified by providing boolean true as the $options argument.
* - See `Pages::find()` $options argument for additional options.
* @return array Array of page IDs, or in verbose mode: array of arrays, each with id, parent_id and templates_id keys.
* @since 3.0.46
*
*/
public function findIDs($selector, $options = array()) {
$verbose = false;
if($options === true) $verbose = true;
if(!is_array($options)) $options = array();
if(isset($options['verbose'])) {
$verbose = $options['verbose'];
unset($options['verbose']);
}
$options['findIDs'] = $verbose ? true : 1;
return $this->find($selector, $options);
}
/**
* Returns the first page matching the given selector with no exclusions
*
@@ -313,12 +365,13 @@ class Pages extends Wire {
* #pw-group-retrieval
*
* @param string|array|Selectors|int $selector Selector string, array or Selectors object. May also be page path or ID.
* @param array $options See `Pages::find()` for extra options that may be specified.
* @return Page|NullPage Always returns a Page object, but will return NullPage (with id=0) when no match found.
* @see Pages::findOne(), Pages::find()
*
*/
public function get($selector) {
return $this->loader->get($selector);
public function get($selector, $options = array()) {
return $this->loader->get($selector, $options);
}
/**
@@ -341,13 +394,14 @@ class Pages extends Wire {
*
* @param Page $page Page object to save
* @param array $options Optional array to modify default behavior, with one or more of the following:
* - `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)
* - `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)
* - `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).
* - `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.
* - `noFields` (boolean): Bypass saving of custom fields, leaving only native properties to be saved (default=false).
* @return bool True on success, false on failure
* @throws WireException
* @see Page::save(), Pages::saveField()
@@ -476,8 +530,11 @@ class Pages extends Wire {
* #pw-group-manipulation
*
* @param Page $page Page to delete
* @param bool $recursive If set to true, then this will attempt to delete all children too.
* @param array $options Optional settings to change behavior (for the future, none currently in use).
* @param bool|array $recursive If set to true, then this will attempt to delete all children too.
* If you don't need this argument, optionally provide $options array instead.
* @param array $options Optional settings to change behavior:
* - uncacheAll (bool): Whether to clear memory cache after delete (default=false)
* - recursive (bool): Same as $recursive argument, may be specified in $options array if preferred.
* @return bool|int Returns true (success), or integer of quantity deleted if recursive mode requested.
* @throws WireException on fatal error
* @see Pages::trash()
@@ -588,6 +645,7 @@ class Pages extends Wire {
* - `findTemplates` (boolean): Determine which templates will be used (when no template specified) for more specific autojoins. (default=true)
* - `pageClass` (string): Class to instantiate Page objects with. Leave blank to determine from template. (default=auto-detect)
* - `pageArrayClass` (string): PageArray-derived class to store pages in (when 'getOne' is false). (default=PageArray)
* - `pageArray` (PageArray|null): Populate this existing PageArray rather than creating a new one. (default=null)
* - `page` (Page|null): Existing Page object to populate (also requires the getOne option to be true). (default=null)
*
* **Use the `$options` array for potential speed optimizations:**
@@ -782,6 +840,79 @@ class Pages extends Wire {
return $this->editor()->touch($pages, $modified);
}
/**
* Set the “sort” value for given $page while adjusting siblings, or re-build sort for its children
*
* *This method is primarily applicable to manually sorted pages. If pages are automatically
* sorted by some other field, this method isnt useful unless using the “re-build children” option,
* which may be helpful if converting a pages children from auto-sort to manual sort.*
*
* The default behavior of this method is to set the “sort” value for the given $page, and adjust the
* sort value of sibling pages having the same or greater sort value, to ensure all are unique and in
* order without gaps.
*
* The alternate behavior of this method is to re-build the sort values of all children of the given $page.
* This is done by specifying boolean true for the $value argument. When used, duplicate sort values and
* gaps are removed from all children.
*
* **Do you need this method?**
* If you are wondering whether you need to use this method for something, chances are that you do not.
* This method is mostly applicable for internal core use, as ProcessWire manages Page sort values on its own
* internally for the most part.
*
* ~~~~~
* // set $page to have sort=5, moving any 5+ sort pages ahead
* $pages->sort($page, 5);
*
* // re-build sort values for children of $page, removing duplicates and gaps
* $pages->sort($page, true);
* ~~~~~
*
* #pw-advanced
*
* @param Page $page Page to sort (or parent of pages to sort, if using $value=true option)
* @param int|bool $value Specify one of the following:
* - Omit to set and use sort value from given $page.
* - Specify sort value (integer) to save that value.
* - Specify boolean true to instead rebuild sort for all of $page children.
* @return int Number of pages that had sort values adjusted
* @throws WireException
*
*/
public function ___sort(Page $page, $value = false) {
if($value === false) $value = $page->sort;
if($value === true) return $this->editor()->sortRebuild($page);
return $this->editor()->sortPage($page, $value);
}
/**
* Sort/move one page above another (for manually sorted pages)
*
* #pw-advanced
*
* @param Page $page Page you want to move/sort
* @param Page $beforePage Page you want to insert before
* @throws WireException
*
*/
public function ___insertBefore(Page $page, Page $beforePage) {
$this->editor()->insertBefore($page, $beforePage);
}
/**
* Sort/move one page after another (for manually sorted pages)
*
* #pw-advanced
*
* @param Page $page Page you want to move/sort
* @param Page $afterPage Page you want to insert after
* @throws WireException
*
*/
public function ___insertAfter(Page $page, Page $afterPage) {
$this->editor()->insertBefore($page, $afterPage, true);
}
/**
* Is the given page in a state where it can be saved from the API?
*
@@ -840,10 +971,11 @@ class Pages extends Wire {
* #pw-internal
*
* @param Page $page
* @return void
*
*/
public function cache(Page $page) {
return $this->cacher->cache($page);
$this->cacher->cache($page);
}
/**
@@ -1065,11 +1197,17 @@ class Pages extends Wire {
*
* #pw-internal
*
* @param array $options Optionally specify array('pageArrayClass' => 'YourPageArrayClass')
* @param array $options Optionally specify ONE of the following:
* - `pageArrayClass` (string): Name of PageArray class to use (if not “PageArray”).
* - `pageArray` (PageArray): Wire and return this given PageArray, rather than instantiating a new one.
* @return PageArray
*
*/
public function newPageArray(array $options = array()) {
if(!empty($options['pageArray']) && $options['pageArray'] instanceof PageArray) {
$this->wire($options['pageArray']);
return $options['pageArray'];
}
$class = 'PageArray';
if(!empty($options['pageArrayClass'])) $class = $options['pageArrayClass'];
$class = wireClassName($class, true);
@@ -1138,7 +1276,7 @@ class Pages extends Wire {
*
*/
public function executeQuery(\PDOStatement $query, $throw = true, $maxTries = 3) {
$this->wire('database')->execute($query, $throw, $maxTries);
return $this->wire('database')->execute($query, $throw, $maxTries);
}
/**
@@ -1149,7 +1287,7 @@ class Pages extends Wire {
* When given an array, it calls $pages->getById($key);
*
* @param string|int|array $key
* @return Page|PageArray
* @return Page|Pages|PageArray
*
*/
public function __invoke($key) {
@@ -1259,7 +1397,9 @@ class Pages extends Wire {
$str = "Saved page";
if(count($changes)) $str .= " (Changes: " . implode(', ', $changes) . ")";
$this->log($str, $page);
$this->wire('cache')->maintenance($page);
/** @var WireCache $cache */
$cache = $this->wire('cache');
$cache->maintenance($page);
if($page->className() != 'Page') {
$manager = $page->getPagesManager();
if($manager instanceof PagesType) $manager->saved($page, $changes, $values);
@@ -1392,7 +1532,9 @@ class Pages extends Wire {
*/
public function ___deleted(Page $page) {
$this->log("Deleted page", $page);
$this->wire('cache')->maintenance($page);
/** @var WireCache $cache */
$cache = $this->wire('cache');
$cache->maintenance($page);
if($page->className() != 'Page') {
$manager = $page->getPagesManager();
if($manager instanceof PagesType) $manager->deleted($page);
@@ -1424,7 +1566,7 @@ class Pages extends Wire {
}
/**
* Hook called when a page has been renamed (i.e. had it's name field change)
* Hook called when a page has been 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`.
@@ -1448,6 +1590,20 @@ class Pages extends Wire {
}
}
/**
* Hook called after a page has been sorted, or had its children re-sorted
*
* #pw-hooker
*
* @param Page $page Page given to have sort adjusted
* @param bool $children If true, children of $page have been all been re-sorted
* @param int $total Total number of pages that had sort adjusted as a result
*
*/
public function ___sorted(Page $page, $children = false, $total = 0) {
if($page && $children && $total) {}
}
/**
* Hook called when a page status has been changed and saved
*

View File

@@ -53,19 +53,25 @@ class PagesAccess extends Wire {
*/
public function __construct($item = null) {
if(!$item) return;
if($item instanceof Page) $this->updatePage($item);
else if($item instanceof Template) $this->updateTemplate($template);
if($item instanceof Page) {
$this->updatePage($item);
} else if($item instanceof Template) {
$this->updateTemplate($item);
}
}
/**
* Rebuild the entire pages_access table (or a part of it) starting from the given parent_id
*
* @param int $parent_id
* @param int $accessTemplateID
* @param bool $doDeletions
*
*/
protected function rebuild($parent_id = 1, $accessTemplateID = 0, $doDeletions = true) {
$insertions = array();
$deletions = array();
$templates = $this->getTemplates();
$accessTemplates = $this->getAccessTemplates();
$parent_id = (int) $parent_id;
$accessTemplateID = (int) $accessTemplateID;
@@ -153,6 +159,8 @@ class PagesAccess extends Wire {
* Update the pages_access table for the given Template
*
* To be called when a template's 'useRoles' property has changed.
*
* @param Template $template
*
*/
public function updateTemplate(Template $template) {
@@ -218,6 +226,8 @@ class PagesAccess extends Wire {
/**
* Delete a page from the pages_access table
*
* @param Page $page
*
*/
public function deletePage(Page $page) {

View File

@@ -15,12 +15,12 @@ class PagesEditor extends Wire {
/**
* Are we currently cloning a page?
*
* This is true only when the clone() method is currently in progress.
* This is greater than 0 only when the clone() method is currently in progress.
*
* @var bool
* @var int
*
*/
protected $cloning = false;
protected $cloning = 0;
/**
* Name for autogenerated page names when fields to generate name aren't populated
@@ -38,10 +38,22 @@ class PagesEditor extends Wire {
public function __construct(Pages $pages) {
$this->pages = $pages;
$config = $pages->wire('config');
if($config->dbStripMB4 && strtolower($config->dbEngine) != 'utf8mb4') {
$this->addHookAfter('Fieldtype::sleepValue', $this, 'hookFieldtypeSleepValueStripMB4');
}
}
public function isCloning() {
return $this->cloning;
/**
* Are we currently in a page clone?
*
* @param bool $getDepth Get depth (int) rather than state (bool)?
* @return bool|int
*
*/
public function isCloning($getDepth = false) {
return $getDepth ? $this->cloning : $this->cloning > 0;
}
/**
@@ -124,9 +136,8 @@ class PagesEditor extends Wire {
public function isSaveable(Page $page, &$reason, $fieldName = '', array $options = array()) {
$saveable = false;
$outputFormattingReason = "Call \$page->setOutputFormatting(false) before getting/setting values that will be modified and saved. ";
$outputFormattingReason = "Call \$page->of(false); before getting/setting values that will be modified and saved.";
$corrupted = array();
$config = $this->wire('config');
if($fieldName && is_object($fieldName)) {
/** @var Field $fieldName */
@@ -143,14 +154,23 @@ class PagesEditor extends Wire {
if($fieldName && !in_array($fieldName, $corrupted)) $corrupted = array();
}
if($page instanceof NullPage) $reason = "Pages of type NullPage are not saveable";
else if((!$page->parent || $page->parent instanceof NullPage) && $page->id !== 1) $reason = "It has no parent assigned";
else if(!$page->template) $reason = "It has no template assigned";
else if(!strlen(trim($page->name)) && $page->id != 1) $reason = "It has an empty 'name' field";
else if(count($corrupted)) $reason = $outputFormattingReason . " [Page::statusCorrupted] fields: " . implode(', ', $corrupted);
else if($page->id == 1 && !$page->template->useRoles) $reason = "Selected homepage template cannot be used because it does not define access.";
else if($page->id == 1 && !$page->template->hasRole('guest')) $reason = "Selected homepage template cannot be used because it does not have the required 'guest' role in it's access settings.";
else $saveable = true;
if($page instanceof NullPage) {
$reason = "Pages of type NullPage are not saveable";
} else if(!$page->parent_id && $page->id !== 1 && (!$page->parent || $page->parent instanceof NullPage)) {
$reason = "It has no parent assigned";
} else if(!$page->template) {
$reason = "It has no template assigned";
} else if(!strlen(trim($page->name)) && $page->id != 1) {
$reason = "It has an empty 'name' field";
} else if(count($corrupted)) {
$reason = $outputFormattingReason . " [Page::statusCorrupted] fields: " . implode(', ', $corrupted);
} else if($page->id == 1 && !$page->template->useRoles) {
$reason = "Selected homepage template cannot be used because it does not define access.";
} else if($page->id == 1 && !$page->template->hasRole('guest')) {
$reason = "Selected homepage template cannot be used because it does not have required 'guest' role in its access settings.";
} else {
$saveable = true;
}
// check if they could corrupt a field by saving
if($saveable && $page->outputFormatting) {
@@ -175,42 +195,71 @@ class PagesEditor extends Wire {
}
}
// FAMILY CHECKS
// check for a parent change and whether it is allowed
if($saveable && $page->parentPrevious && $page->parentPrevious->id != $page->parent->id && empty($options['ignoreFamily'])) {
// page was moved
if($page->template->noMove && ($page->hasStatus(Page::statusSystem) || $page->hasStatus(Page::statusSystemID) || !$page->isTrash())) {
// make sure the page's template allows moves. only move laways allowed is to the trash, unless page has system status
$saveable = false;
$reason = "Pages using template '{$page->template}' are not moveable (template::noMove) [{$page->parentPrevious->path} => {$page->parent->path}]";
} else if($page->parent->template->noChildren) {
$saveable = false;
$reason = "Chosen parent '{$page->parent->path}' uses template that does not allow children.";
} else if($page->parent->id && $page->parent->id != $config->trashPageID && count($page->parent->template->childTemplates)
&& !in_array($page->template->id, $page->parent->template->childTemplates)) {
// make sure the new parent's template allows pages with this template
$saveable = false;
$reason =
"Can't move '{$page->name}' because Template '{$page->parent->template}' used by '{$page->parent->path}' " .
"doesn't allow children with this template.";
} else if(count($page->template->parentTemplates) && $page->parent->id != $config->trashPageID
&& !in_array($page->parent->template->id, $page->template->parentTemplates)) {
$saveable = false;
$reason =
"Can't move '{$page->name}' because Template '{$page->parent->template}' used by '{$page->parent->path}' " .
"is not allowed by template '{$page->template->name}'.";
} else if(count($page->parent->children("name={$page->name}, id!=$page->id, include=all"))) {
$saveable = false;
$reason = "Chosen parent '{$page->parent->path}' already has a page named '{$page->name}'";
}
if($saveable && $page->parentPrevious && empty($options['ignoreFamily'])) {
// parent has changed, check that the move is allowed
$saveable = $this->isMoveable($page, $page->parentPrevious, $page->parent, $reason);
}
return $saveable;
}
/**
* Return whether given Page is moveable from $oldParent to $newParent
*
* @param Page $page Page to move
* @param Page $oldParent Current/old parent page
* @param Page $newParent New requested parent page
* @param string $reason Populated with reason why page is not moveable, if return false is false.
* @return bool
*
*/
public function isMoveable(Page $page, Page $oldParent, Page $newParent, &$reason) {
if($oldParent->id == $newParent->id) return true;
$config = $this->wire('config');
$moveable = false;
// page was moved
if($page->template->noMove
&& ($page->hasStatus(Page::statusSystem) || $page->hasStatus(Page::statusSystemID) || !$page->isTrash())) {
// make sure the page template allows moves.
// only move always allowed is to the trash, unless page has system status
$reason =
"Page using template '$page->template' is not moveable " .
"(Template::noMove) [{$oldParent->path} => {$newParent->path}].";
} else if($newParent->template->noChildren) {
// check if new parent disallows children
$reason =
"Chosen parent '$newParent->path' uses template '$newParent->template' that does not allow children.";
} else if($newParent->id && $newParent->id != $config->trashPageID && count($newParent->template->childTemplates)
&& !in_array($page->template->id, $newParent->template->childTemplates)) {
// make sure the new parent's template allows pages with this template
$reason =
"Cannot move '$page->name' because template '$newParent->template' used by page '$newParent->path' " .
"does not allow children using template '$page->template'.";
} else if(count($page->template->parentTemplates) && $newParent->id != $config->trashPageID
&& !in_array($newParent->template->id, $page->template->parentTemplates)) {
// check for allowed parentTemplates setting
$reason =
"Cannot move '$page->name' because template '$newParent->template' used by new parent '$newParent->path' " .
"is not allowed by moved page template '$page->template'.";
} else if(count($newParent->children("name=$page->name, id!=$page->id, include=all"))) {
// check for page name collision
$reason =
"Chosen parent '$newParent->path' already has a page named '$page->name'.";
} else {
$moveable = true;
}
return $moveable;
}
/**
* Is the given page deleteable from the API?
@@ -428,6 +477,7 @@ class PagesEditor extends Wire {
* - `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)
* @return bool True on success, false on failure
* @throws WireException
*
@@ -441,6 +491,7 @@ class PagesEditor extends Wire {
'forceID' => 0,
'ignoreFamily' => false,
'noHooks' => false,
'noFields' => false,
);
if(is_string($options)) $options = Selectors::keyValueStringToArray($options);
@@ -461,7 +512,7 @@ class PagesEditor extends Wire {
if(!$this->isSaveable($page, $reason, '', $options)) {
if($language) $user->language = $language;
throw new WireException("Can't save page {$page->id}: {$page->path}: $reason");
throw new WireException("Cant save page {$page->id}: {$page->path}: $reason");
}
if($page->hasStatus(Page::statusUnpublished) && $page->template->noUnpublish) {
@@ -648,7 +699,7 @@ class PagesEditor extends Wire {
*/
protected function savePageFinish(Page $page, $isNew, array $options) {
$changes = $page->getChanges();
$changes = $page->getChanges(2);
$changesValues = $page->getChanges(true);
// update children counts for current/previous parent
@@ -683,14 +734,17 @@ class PagesEditor extends Wire {
// save each individual Fieldtype data in the fields_* tables
foreach($page->fieldgroup as $field) {
if(isset($corruptedFields[$field->name])) continue; // don't even attempt save of corrupted field
if(!$field->type) continue;
if(!$page->hasField($field)) continue; // field not valid for page
try {
$field->type->savePageField($page, $field);
} catch(\Exception $e) {
$error = sprintf($this->_('Error saving field "%s"'), $field->name) . ' - ' . $e->getMessage();
$this->trackException($e, true, $error);
$name = $field->name;
if($options['noFields'] || isset($corruptedFields[$name]) || !$field->type || !$page->hasField($field)) {
unset($changes[$name]);
unset($changesValues[$name]);
} else {
try {
$field->type->savePageField($page, $field);
} catch(\Exception $e) {
$error = sprintf($this->_('Error saving field "%s"'), $name) . ' - ' . $e->getMessage();
$this->trackException($e, true, $error);
}
}
}
@@ -698,7 +752,17 @@ class PagesEditor extends Wire {
$page->of($of);
if(empty($page->template->sortfield)) $this->pages->sortfields()->save($page);
if($options['resetTrackChanges']) $page->resetTrackChanges();
if($options['resetTrackChanges']) {
if($options['noFields']) {
// reset for only fields that were saved
foreach($changes as $change) $page->untrackChange($change);
$page->setTrackChanges(true);
} else {
// reset all changes
$page->resetTrackChanges();
}
}
// determine whether we'll trigger the added() hook
if($isNew) {
@@ -969,24 +1033,34 @@ class PagesEditor extends Wire {
* this method will throw an exception. If a recursive delete fails for any reason, an exception will be thrown.
*
* @param Page $page
* @param bool $recursive If set to true, then this will attempt to delete all children too.
* @param array $options Optional settings to change behavior (for the future)
* @param bool|array $recursive If set to true, then this will attempt to delete all children too.
* If you don't need this argument, optionally provide $options array instead.
* @param array $options Optional settings to change behavior:
* - uncacheAll (bool): Whether to clear memory cache after delete (default=false)
* - recursive (bool): Same as $recursive argument, may be specified in $options array if preferred.
* @return bool|int Returns true (success), or integer of quantity deleted if recursive mode requested.
* @throws WireException on fatal error
*
*/
public function delete(Page $page, $recursive = false, array $options = array()) {
$defaults = array(
'uncacheAll' => false,
'recursive' => is_bool($recursive) ? $recursive : false,
);
if(is_array($recursive)) $options = $recursive;
$options = array_merge($defaults, $options);
if($options) {} // to ignore unused parameter inspection
if(!$this->isDeleteable($page)) throw new WireException("This page may not be deleted");
$numDeleted = 0;
if($page->numChildren) {
if(!$recursive) {
if(!$options['recursive']) {
throw new WireException("Can't delete Page $page because it has one or more children.");
} else foreach($page->children("include=all") as $child) {
/** @var Page $child */
if($this->pages->delete($child, true)) {
if($this->pages->delete($child, true, $options)) {
$numDeleted++;
} else {
throw new WireException("Error doing recursive page delete, stopped by page $child");
@@ -1008,6 +1082,7 @@ class PagesEditor extends Wire {
} catch(\Exception $e) {
}
/** @var PagesAccess $access */
$access = $this->wire(new PagesAccess());
$access->deletePage($page);
@@ -1026,14 +1101,14 @@ class PagesEditor extends Wire {
$page->status = Page::statusDeleted; // no need for bitwise addition here, as this page is no longer relevant
$this->pages->deleted($page);
$numDeleted++;
$this->pages->uncacheAll($page);
if($options['uncacheAll']) $this->pages->uncacheAll($page);
$this->pages->debugLog('delete', $page, true);
return $recursive ? $numDeleted : true;
return $options['recursive'] ? $numDeleted : true;
}
/**
* Clone an entire page, it's assets and children and return it.
* 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)
@@ -1051,23 +1126,27 @@ class PagesEditor extends Wire {
if(is_string($options)) $options = Selectors::keyValueStringToArray($options);
if(!isset($options['recursionLevel'])) $options['recursionLevel'] = 0; // recursion level
// if parent is not changing, we have to modify name now
if(is_null($parent)) {
$parent = $page->parent;
$n = 1;
$name = $page->name . '-' . $n;
if(isset($options['set']) && isset($options['set']['name'])) {
$name = $options['set']['name'];
} else {
$name = $page->name;
$n = 0;
}
// if parent is not changing, we have to modify name now
if(is_null($parent) || $parent->id == $page->parent->id) {
$parent = $page->parent;
$n = 1;
$name = $page->name . '-' . $n;
} else {
$name = $page->name;
$n = 0;
}
// make sure that we have a unique name
while(count($parent->children("name=$name, include=all"))) {
$name = $page->name;
$nStr = "-" . (++$n);
if(strlen($name) + strlen($nStr) > Pages::nameMaxLength) $name = substr($name, 0, Pages::nameMaxLength - strlen($nStr));
$name .= $nStr;
// make sure that we have a unique name
while(count($parent->children("name=$name, include=all"))) {
$name = $page->name;
$nStr = "-" . (++$n);
if(strlen($name) + strlen($nStr) > Pages::nameMaxLength) $name = substr($name, 0, Pages::nameMaxLength - strlen($nStr));
$name .= $nStr;
}
}
// Ensure all data is loaded for the page
@@ -1077,6 +1156,7 @@ class PagesEditor extends Wire {
// clone in memory
$copy = clone $page;
$copy->setQuietly('_cloning', $page);
$copy->id = isset($options['forceID']) ? (int) $options['forceID'] : 0;
$copy->setIsNew(true);
$copy->name = $name;
@@ -1113,19 +1193,23 @@ class PagesEditor extends Wire {
$o = $copy->outputFormatting;
$copy->setOutputFormatting(false);
$this->pages->cloneReady($page, $copy);
$this->cloning++;
$options['ignoreFamily'] = true; // skip family checks during clone
try {
$this->cloning = true;
$options['ignoreFamily'] = true; // skip family checks during clone
$this->pages->save($copy, $options);
} catch(\Exception $e) {
$this->cloning = false;
$this->cloning--;
$copy->setQuietly('_cloning', null);
throw $e;
}
$this->cloning = false;
$this->cloning--;
$copy->setOutputFormatting($o);
// check to make sure the clone has worked so far
if(!$copy->id || $copy->id == $page->id) return $this->pages->newNullPage();
if(!$copy->id || $copy->id == $page->id) {
$copy->setQuietly('_cloning', null);
return $this->pages->newNullPage();
}
// copy $page's files over to new page
if(PagefilesManager::hasFiles($page)) {
@@ -1133,7 +1217,7 @@ class PagesEditor extends Wire {
$page->filesManager->copyFiles($copy->filesManager->path());
}
// if there are children, then recurisvely clone them too
// if there are children, then recursively clone them too
if($page->numChildren && $recursive) {
$start = 0;
$limit = 200;
@@ -1155,10 +1239,18 @@ class PagesEditor extends Wire {
if($recursive && $options['recursionLevel'] === 0) {
$this->saveParents($copy->id, $copy->numChildren);
}
if($options['recursionLevel'] === 0) {
if($copy->parent()->sortfield() == 'sort') {
$this->sortPage($copy, $copy->sort, true);
}
}
$copy->setQuietly('_cloning', null);
$copy->resetTrackChanges();
$this->pages->cloned($page, $copy);
$this->pages->debugLog('clone', "page=$page, parent=$parent", $copy);
// $copy->setTrackChanges(false);
return $copy;
}
@@ -1210,4 +1302,200 @@ class PagesEditor extends Wire {
return $this->wire('database')->execute($query);
}
}
/**
* Move page to specified parent (work in progress)
*
* This method is the same as changing a page parent and saving, but provides a useful shortcut
* for some cases with less code. This method:
*
* - Does not save the other custom fields of a page (if any are changed).
* - Does not require that output formatting be off (it manages that internally).
*
* @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.
* @throws WireException if given parent does not exist, or move is not allowed
*
*/
public function move(Page $child, $parent, array $options = array()) {
if(is_string($parent) || is_int($parent)) $parent = $this->pages->get($parent);
if(!$parent instanceof Page || !$parent->id) throw new WireException('Unable to locate parent for move');
$options['noFields'] = true;
$of = $child->of();
$child->of(false);
$child->parent = $parent;
$result = $child->parentPrevious ? $this->pages->save($child, $options) : false;
if($of) $child->of(true);
return $result;
}
/**
* Set page $sort value and increment siblings having same or greater sort value
*
* - This method is primarily applicable if configured sortfield is manual “sort” (or “none”).
* - This is typically used after a move, sort, clone or delete operation.
*
* @param Page $page Page that you want to set the sort value for
* @param int|null $sort New sort value for page or null to pull from $page->sort
* @param bool $after If another page already has the sort, make $page go after it rather than before it? (default=false)
* @throws WireException if given invalid arguments
* @return int Number of sibling pages that had to have sort adjusted
*
*/
public function sortPage(Page $page, $sort = null, $after = false) {
$database = $this->wire('database');
// reorder siblings having same or greater sort value, when necessary
if($page->id <= 1) return 0;
if(is_null($sort)) $sort = $page->sort;
// determine if any other siblings have same sort value
$sql = 'SELECT id FROM pages WHERE parent_id=:parent_id AND sort=:sort AND id!=:id';
$query = $database->prepare($sql);
$query->bindValue(':parent_id', $page->parent_id, \PDO::PARAM_INT);
$query->bindValue(':sort', $sort, \PDO::PARAM_INT);
$query->bindValue(':id', $page->id, \PDO::PARAM_INT);
$query->execute();
$rowCount = $query->rowCount();
$query->closeCursor();
// move sort to after if requested
if($after && $rowCount) $sort += $rowCount;
// update $page->sort property if needed
if($page->sort != $sort) $page->sort = $sort;
// make sure that $page has the sort value indicated
$sql = 'UPDATE pages SET sort=:sort WHERE id=:id';
$query = $database->prepare($sql);
$query->bindValue(':sort', $sort, \PDO::PARAM_INT);
$query->bindValue(':id', $page->id, \PDO::PARAM_INT);
$query->execute();
$sortCnt = $query->rowCount();
// no need for $page to have 'sort' indicated as a change, since we just updated it above
$page->untrackChange('sort');
if($rowCount) {
// update order of all siblings
$sql = 'UPDATE pages SET sort=sort+1 WHERE parent_id=:parent_id AND sort>=:sort AND id!=:id';
$query = $database->prepare($sql);
$query->bindValue(':parent_id', $page->parent_id, \PDO::PARAM_INT);
$query->bindValue(':sort', $sort, \PDO::PARAM_INT);
$query->bindValue(':id', $page->id, \PDO::PARAM_INT);
$query->execute();
$sortCnt += $query->rowCount();
}
// call the sorted hook
$this->pages->sorted($page, false, $sortCnt);
return $sortCnt;
}
/**
* Sort one page before another (for pages using manual sort)
*
* Note that if given $sibling parent is different from `$page` parent, then the `$pages->save()`
* method will also be called to perform that movement.
*
* @param Page $page Page to move/sort
* @param Page $sibling Sibling that page will be moved/sorted before
* @param bool $after Specify true to make $page move after $sibling instead of before (default=false)
* @throws WireException When conditions don't allow page insertions
*
*/
public function insertBefore(Page $page, Page $sibling, $after = false) {
$sortfield = $sibling->parent()->sortfield();
if($sortfield != 'sort') {
throw new WireException('Insert before/after operations can only be used with manually sorted pages');
}
if(!$sibling->id || !$page->id) {
throw new WireException('New pages must be saved before using insert before/after operations');
}
if($sibling->id == 1 || $page->id == 1) {
throw new WireException('Insert before/after operations cannot involve homepage');
}
$page->sort = $sibling->sort;
if($page->parent_id != $sibling->parent_id) {
// page needs to be moved first
$page->parent = $sibling->parent;
$page->save();
}
$this->sortPage($page, $page->sort, $after);
}
/**
* Rebuild the “sort” values for all children of the given $parent page, fixing duplicates and gaps
*
* If used on a $parent not currently sorted by by “sort” then it will update the “sort” index to be
* consistent with whatever the pages are sorted by.
*
* @param Page $parent
* @return int
*
*/
public function sortRebuild(Page $parent) {
if(!$parent->id || !$parent->numChildren) return 0;
$database = $this->wire('database');
$sorts = array();
$sort = 0;
if($parent->sortfield() == 'sort') {
// pages are manually sorted, so we can find IDs directly from the database
$sql = 'SELECT id FROM pages WHERE parent_id=:parent_id ORDER BY sort, created';
$query = $database->prepare($sql);
$query->bindValue(':parent_id', $parent->id, \PDO::PARAM_INT);
$query->execute();
// establish new sort values
do {
$id = (int) $query->fetch(\PDO::FETCH_COLUMN);
if(!$id) break;
$sorts[] = "($id,$sort)";
} while(++$sort);
$query->closeCursor();
} else {
// children of $parent don't currently use "sort" as sort property
// so we will update the "sort" of children to be consistent with that
// of whatever sort property is in use.
$o = array('findIDs' => 1, 'cache' => false);
foreach($parent->children('include=all', $o) as $id) {
$id = (int) $id;
$sorts[] = "($id,$sort)";
$sort++;
}
}
// update sort values
$query = $database->prepare(
'INSERT INTO pages (id,sort) VALUES ' . implode(',', $sorts) . ' ' .
'ON DUPLICATE KEY UPDATE sort=VALUES(sort)'
);
$query->execute();
return count($sorts);
}
/**
* Hook after Fieldtype::sleepValue to remove MB4 characters when present and applicable
*
* This hook is only used if $config->dbStripMB4 is true and $config->dbEngine is not “utf8mb4”.
*
* @param HookEvent $event
*
*/
protected function hookFieldtypeSleepValueStripMB4(HookEvent $event) {
$event->return = $this->wire('sanitizer')->removeMB4($event->return);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -40,6 +40,14 @@ class PagesLoader extends Wire {
*/
protected $nativeColumns = array();
/**
* Total number of pages loaded by getById()
*
* @var int
*
*/
protected $totalPagesLoaded = 0;
/**
* Debug mode for pages class
*
@@ -106,15 +114,20 @@ class PagesLoader extends Wire {
if(empty($selector)) return $this->pages->newPageArray($loadOptions);
if(!empty($options['lazy'])) return false;
if(is_array($selector)) {
$value = false;
$filter = empty($options['findOne']);
if(is_array($selector)) {
if(ctype_digit(implode('', array_keys($selector))) && !is_array(reset($selector)) && ctype_digit(implode('', $selector))) {
// if given a regular array of page IDs, we delegate that to getById() method, but with access/visibility control
return $this->filterListable(
$this->getById($selector),
(isset($options['include']) ? $options['include'] : ''),
$loadOptions);
$value = $this->getById($selector, $loadOptions);
$filter = true;
}
} else if(is_int($selector)) {
$value = $this->getById(array($selector), $loadOptions);
} else if(is_string($selector) || is_int($selector)) {
@@ -129,20 +142,28 @@ class PagesLoader extends Wire {
if(ctype_digit("$selector") || strpos($selector, "id=") === 0) {
// if selector is just a number, or a string like "id=123" then we're going to do a shortcut
$s = str_replace("id=", '', $selector);
if(ctype_digit("$s")) {
$value = $this->getById(array((int) $s), $loadOptions);
if(empty($options['findOne'])) $value = $this->filterListable(
$value, (isset($options['include']) ? $options['include'] : ''), $loadOptions);
if($this->debug) $this->pages->debugLog('find', $selector . " [optimized]", $value);
return $value;
if(ctype_digit(str_replace('|', '', "$s"))) {
$a = explode('|', $s);
foreach($a as $k => $v) $a[$k] = (int) $v;
$value = $this->getById($a, $loadOptions);
}
}
}
}
return false;
}
if($value) {
if($filter) {
$includeMode = isset($options['include']) ? $options['include'] : '';
$value = $this->filterListable($value, $includeMode, $loadOptions);
}
if($this->debug) {
$this->pages->debugLog('find', $selector . " [optimized]", $value);
}
}
return $value;
}
/**
* Given a Selector string, return the Page objects that match in a PageArray.
*
@@ -152,8 +173,11 @@ class PagesLoader extends Wire {
* @param string|int|array|Selectors $selector Specify selector (standard usage), but can also accept page ID or array of page IDs.
* @param array|string $options Optional one or more options that can modify certain behaviors. May be assoc array or key=value string.
* - findOne: boolean - apply optimizations for finding a single page
* - findAll: boolean - find all pages with no exculsions (same as include=all option)
* - findAll: boolean - find all pages with no exclusions (same as include=all option)
* - findIDs: boolean|int - true=return array of [id, template_id, parent_id], or 1=return just page IDs.
* - getTotal: boolean - whether to set returning PageArray's "total" property (default: true except when findOne=true)
* - cache: boolean - Allow caching of selectors and pages loaded (default=true). Also sets loadOptions[cache].
* - allowCustom: boolean - Whether to allow use of "_custom=new selector" in selectors (default=false).
* - lazy: boolean - makes find() return Page objects that don't have any data populated to them (other than id and template).
* - loadPages: boolean - whether to populate the returned PageArray with found pages (default: true).
* The only reason why you'd want to change this to false would be if you only needed the count details from
@@ -176,10 +200,15 @@ class PagesLoader extends Wire {
$loadPages = array_key_exists('loadPages', $options) ? (bool) $options['loadPages'] : true;
$caller = isset($options['caller']) ? $options['caller'] : 'pages.find';
$lazy = empty($options['lazy']) ? false : true;
$findIDs = isset($options['findIDs']) ? $options['findIDs'] : false;
$debug = $this->debug && !$lazy;
$pages = $this->findShortcut($selector, $options, $loadOptions);
$cachePages = isset($options['cache']) ? (bool) $options['cache'] : true;
if(!$cachePages && !isset($loadOptions['cache'])) $loadOptions['cache'] = false;
if($pages) return $pages;
if($loadPages && !$lazy && !$findIDs) {
$pages = $this->findShortcut($selector, $options, $loadOptions);
if($pages) return $pages;
}
if($selector instanceof Selectors) {
$selectors = $selector;
@@ -195,10 +224,12 @@ class PagesLoader extends Wire {
$selectorString = is_string($selector) ? $selector : (string) $selectors;
// see if this has been cached and return it if so
$pages = $this->pages->cacher()->getSelectorCache($selectorString, $options);
if(!is_null($pages)) {
if($debug) $this->pages->debugLog('find', $selectorString, $pages . ' [from-cache]');
return $pages;
if($loadPages && !$findIDs && !$lazy) {
$pages = $this->pages->cacher()->getSelectorCache($selectorString, $options);
if(!is_null($pages)) {
if($debug) $this->pages->debugLog('find', $selectorString, $pages . ' [from-cache]');
return $pages;
}
}
$pageFinder = $this->pages->getPageFinder();
@@ -209,12 +240,17 @@ class PagesLoader extends Wire {
$profiler = $this->wire('profiler');
$profilerEvent = $profiler ? $profiler->start("$caller($selectorString)", "Pages") : null;
if($lazy) {
if(strpos($selectorString, 'limit=') === false) $options['getTotal'] = false;
if(($lazy || $findIDs) && strpos($selectorString, 'limit=') === false) $options['getTotal'] = false;
if($lazy || $findIDs === 1) {
$pagesIDs = $pageFinder->findIDs($selectors, $options);
} else {
$pagesInfo = $pageFinder->find($selectors, $options);
}
if($debug && empty($loadOptions['caller'])) {
$loadOptions['caller'] = "$caller($selectorString)";
}
// note that we save this pagination state here and set it at the end of this method
// because it's possible that more find operations could be executed as the pages are loaded
@@ -228,17 +264,25 @@ class PagesLoader extends Wire {
$pages->finderOptions($options);
$pages->setDuplicateChecking(false);
$loadPages = false;
$cachePages = false;
$template = null;
foreach($pagesIDs as $id) {
$page = $this->pages->newPage();
$page->_lazy($id);
$page->loaderCache = false;
$pages->add($page);
}
$pages->setDuplicateChecking(true);
if(count($pagesIDs)) $pages->_lazy(true);
} else if($findIDs) {
$loadPages = false;
$cachePages = false;
$pages = $this->pages->newPageArray($loadOptions); // only for hooks to see
} else if($loadPages) {
// parent_id is null unless a single parent was specified in the selectors
$parent_id = $pageFinder->getParentID();
@@ -255,6 +299,7 @@ class PagesLoader extends Wire {
if(count($idsByTemplate) > 1) {
// perform a load for each template, which results in unsorted pages
// @todo use $idsUnsorted array rather than $unsortedPages PageArray
$unsortedPages = $this->pages->newPageArray($loadOptions);
foreach($idsByTemplate as $tpl_id => $ids) {
$opt = $loadOptions;
@@ -282,18 +327,24 @@ class PagesLoader extends Wire {
$opt['parent_id'] = $parent_id;
$pages->import($this->getById($idsSorted, $opt));
}
$sortsAfter = $pageFinder->getSortsAfter();
if(count($sortsAfter)) $pages->sort($sortsAfter);
} else {
$pages = $this->pages->newPageArray($loadOptions);
}
$pageFinder->getPageArrayData($pages);
$pages->setTotal($total);
$pages->setLimit($limit);
$pages->setStart($start);
$pages->setSelectors($selectors);
$pages->setTrackChanges(true);
if($loadPages) $this->pages->cacher()->selectorCache($selectorString, $options, $pages);
if($loadPages && $cachePages) {
$this->pages->cacher()->selectorCache($selectorString, $options, $pages);
}
if($debug) {
$this->pages->debugLog('find', $selectorString, $pages);
@@ -303,10 +354,11 @@ class PagesLoader extends Wire {
$note .= ": " . $pages->first()->path;
if($count > 1) $note .= " ... " . $pages->last()->path;
}
Debug::saveTimer("$caller($selectorString)", $note);
if(substr($caller, -1) !== ')') $caller .= "($selectorString)";
Debug::saveTimer($caller, $note);
foreach($pages as $item) {
if($item->_debug_loaded) continue;
$item->setQuietly('_debug_loader', "$caller($selectorString)");
if($item->_debug_loader) continue;
$item->setQuietly('_debug_loader', $caller);
}
}
@@ -317,6 +369,8 @@ class PagesLoader extends Wire {
'pagesInfo' => $pagesInfo,
'options' => $options
));
if($findIDs) return $findIDs === 1 ? $pagesIDs : $pagesInfo;
return $pages;
}
@@ -352,21 +406,23 @@ class PagesLoader extends Wire {
* Returns the first page matching the given selector with no exclusions
*
* @param string|int|array|Selectors $selector
* @param array $options See Pages::find method for options
* @return Page|NullPage Always returns a Page object, but will return NullPage (with id=0) when no match found
*
*/
public function get($selector) {
public function get($selector, $options = array()) {
if(empty($selector)) return $this->pages->newNullPage();
if(is_string($selector) || is_int($selector)) {
$page = $this->pages->getCache($selector);
if($page) return $page;
}
$options = array(
$defaults = array(
'findOne' => true, // find only one page
'findAll' => true, // no exclusions
'getTotal' => false, // don't count totals
'caller' => 'pages.get'
);
$options = count($options) ? array_merge($defaults, $options) : $defaults;
$page = $this->pages->find($selector, $options)->first();
if(!$page) $page = $this->pages->newNullPage();
return $page;
@@ -391,7 +447,9 @@ class PagesLoader extends Wire {
* - findTemplates: boolean, default=true. Determine which templates will be used (when no template specified) for more specific autojoins.
* - pageClass: string, default=auto-detect. Class to instantiate Page objects with. Leave blank to determine from template.
* - pageArrayClass: string, default=PageArray. PageArray-derived class to store pages in (when 'getOne' is false).
* - pageArray: PageArray, default=null. Optional predefined PageArray to populate to.
* - page (Page|null): Existing Page object to populate (also requires the getOne option to be true). (default=null)
* - caller (string): Name of calling function, for debugging purposes (default=blank).
*
* Use the $options array for potential speed optimizations:
* - Specify a 'template' with your call, when possible, so that this method doesn't have to determine it separately.
@@ -427,7 +485,9 @@ class PagesLoader extends Wire {
'joinFields' => array(),
'page' => null,
'pageClass' => '', // blank = auto detect
'pageArray' => null, // PageArray to populate to
'pageArrayClass' => 'PageArray',
'caller' => '',
);
if(is_array($template)) {
@@ -582,16 +642,22 @@ class PagesLoader extends Wire {
if($joinSortfield) $query->leftjoin('pages_sortfields ON pages_sortfields.pages_id=pages.id');
$query->groupby('pages.id');
if($options['autojoin'] && $this->autojoin) foreach($fields as $field) {
if(!empty($options['joinFields']) && in_array($field->name, $options['joinFields'])) {
// joinFields option specified to force autojoin this field
} else {
if(!($field->flags & Field::flagAutojoin)) continue; // autojoin not enabled for field
if($fields instanceof Fields && !($field->flags & Field::flagGlobal)) continue; // non-fieldgroup, autojoin only if global flag is set
if($options['autojoin'] && $this->autojoin) {
foreach($fields as $field) {
if(!empty($options['joinFields']) && in_array($field->name, $options['joinFields'])) {
// joinFields option specified to force autojoin this field
} else {
// check if autojoin not enabled for field
if(!($field->flags & Field::flagAutojoin)) continue;
// non-fieldgroup, autojoin only if global flag is set
if($fields instanceof Fields && !($field->flags & Field::flagGlobal)) continue;
}
$table = $database->escapeTable($field->table);
// check autojoin not allowed, otherwise merge in the autojoin query
if(!$field->type || !$field->type->getLoadQueryAutojoin($field, $query)) continue;
// complete autojoin
$query->leftjoin("$table ON $table.pages_id=pages.id"); // QA
}
$table = $database->escapeTable($field->table);
if(!$field->type || !$field->type->getLoadQueryAutojoin($field, $query)) continue; // autojoin not allowed
$query->leftjoin("$table ON $table.pages_id=pages.id"); // QA
}
if(!is_null($parent_id)) $query->where("pages.parent_id=" . (int) $parent_id);
@@ -611,9 +677,16 @@ class PagesLoader extends Wire {
$class = 'Page';
}
}
if($class != 'Page' && !wireClassExists($class)) {
$this->error("Class '$class' for Pages::getById() does not exist", Notice::log);
$class = 'Page';
$_class = wireClassName($class, true);
if($class != 'Page' && !wireClassExists($_class)) {
if(class_exists("\\$class")) {
$_class = "\\$class";
} else {
$this->error("Class '$class' for Pages::getById() does not exist", Notice::log);
$class = 'Page';
$_class = wireClassName($class, true);
}
}
// page to populate, if provided in 'getOne' mode
@@ -621,7 +694,6 @@ class PagesLoader extends Wire {
$_page = $options['getOne'] && $options['page'] && $options['page'] instanceof Page ? $options['page'] : null;
try {
$_class = wireClassName($class, true);
// while($page = $stmt->fetchObject($_class, array($template))) {
/** @noinspection PhpAssignmentInConditionInspection */
while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
@@ -638,6 +710,7 @@ class PagesLoader extends Wire {
}
unset($row['templates_id']);
foreach($row as $key => $value) $page->set($key, $value);
if($options['cache'] === false) $page->loaderCache = false;
$page->instanceID = ++$instanceID;
$page->setIsLoaded(true);
$page->setIsNew(false);
@@ -645,6 +718,7 @@ class PagesLoader extends Wire {
$page->setOutputFormatting($this->outputFormatting);
$loaded[$page->id] = $page;
if($options['cache']) $this->pages->cache($page);
$this->totalPagesLoaded++;
}
} catch(\Exception $e) {
$error = $e->getMessage() . " [pageClass=$class, template=$template]";
@@ -668,11 +742,18 @@ class PagesLoader extends Wire {
if($this->debug) {
$page = $this->wire('page');
if($page && $page->template == 'admin') {
$_template = is_null($template) ? '' : ", $template";
$_parent_id = is_null($parent_id) ? '' : ", $parent_id";
$_ids = count($_ids) > 1 ? "[" . implode(',', $_ids) . "]" : implode('', $_ids);
if(empty($options['caller'])) {
$_template = is_null($template) ? '' : ", $template";
$_parent_id = is_null($parent_id) ? '' : ", $parent_id";
if(count($_ids) > 10) {
$_ids = '[' . reset($_ids) . '…' . end($_ids) . ', ' . count($_ids) . ' pages]';
} else {
$_ids = count($_ids) > 1 ? "[" . implode(',', $_ids) . "]" : implode('', $_ids);
}
$options['caller'] = "pages.getById($_ids$_template$_parent_id)";
}
foreach($pages as $item) {
$item->setQuietly('_debug_loader', "getByID($_ids$_template$_parent_id)");
$item->setQuietly('_debug_loader', $options['caller']);
}
}
}
@@ -738,9 +819,9 @@ class PagesLoader extends Wire {
// if page is already loaded and cache allowed, then get the path from it
if($options['useCache'] && $page = $this->pages->getCache($id)) {
/** @var Page $page */
if($languages) $languages->setLanguage($language);
if($languageID) $languages->setLanguage($language);
$path = $page->path();
if($languages) $languages->unsetLanguage();
if($languageID) $languages->unsetLanguage();
return $path;
} else if($id == $homepageID && $languages && !$languageID) {
@@ -1069,5 +1150,15 @@ class PagesLoader extends Wire {
if(!is_null($debug)) $this->debug = (bool) $debug;
return $value;
}
/**
* Return the total quantity of pages loaded by getById()
*
* @return int
*
*/
public function getTotalPagesLoaded() {
return $this->totalPagesLoaded;
}
}

View File

@@ -66,6 +66,7 @@ class PagesLoaderCache extends Wire {
* Cache the given page.
*
* @param Page $page
* @return void
*
*/
public function cache(Page $page) {
@@ -155,6 +156,31 @@ class PagesLoaderCache extends Wire {
return true;
}
/**
* Convert an options array to a string
*
* @param array $options
* @return string
*
*/
protected function optionsArrayToString(array $options) {
$str = '';
ksort($options);
foreach($options as $key => $value) {
if(is_array($value)) {
$value = $this->optionsArrayToString($value);
} else if(is_object($value)) {
if(method_exists($value, '__toString')) {
$value = (string) $value;
} else {
$value = wireClassName($value);
}
}
$str .= "[$key:$value]";
}
return $str;
}
/**
* Retrieve any cached page IDs for the given selector and options OR false if none found.
*
@@ -163,18 +189,13 @@ class PagesLoaderCache extends Wire {
* @param string $selector
* @param array $options
* @param bool $returnSelector default false
* @return array|null|string
* @return array|null|string|PageArray
*
*/
public function getSelectorCache($selector, $options, $returnSelector = false) {
if(count($options)) {
$optionsHash = '';
ksort($options);
foreach($options as $key => $value) {
if(is_array($value)) $value = print_r($value, true);
$optionsHash .= "[$key:$value]";
}
$optionsHash = $this->optionsArrayToString($options);
$selector .= "," . $optionsHash;
} else {
$selector .= ",";

View File

@@ -3,8 +3,15 @@
/**
* ProcessWire PagesType
*
* Provides an interface to the Pages class but specific to
* a given page class/type, with predefined parent and template.
* #pw-summary Provides an interface to the Pages class but specific to a given page class/type, with predefined parent and template.
* #pw-body =
* This class is primarily used by the core as an alternative to `$pages`, providing an API for other Page types like
* `User`, `Role`, `Permission`, and `Language`. The `$users`, `$roles`, `$permissions` and `$languages` API variables
* are all instances of `PagesType`. This class is typically not instantiated on its own and instead acts as a base class
* which is extended.
*
* #pw-body
* #pw-use-constructor
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* https://processwire.com
@@ -83,6 +90,8 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
/**
* Add one or more templates that this PagesType represents
*
* #pw-group-family
*
* @param array|int|string $templates Single or array of Template objects, IDs, or names
*
*/
@@ -110,6 +119,8 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
/**
* Add one or more of parents that this PagesType represents
*
* #pw-group-family
*
* @param array|int|string|Page $parents Single or array of Page objects, IDs, or paths
*
*/
@@ -170,6 +181,8 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
/**
* Is the given page a valid type for this class?
*
* #pw-internal
*
* @param Page $page
* @return bool
@@ -233,15 +246,17 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
* Given a Selector string, return the Page objects that match in a PageArray.
*
* @param string $selectorString
* @param array $options
- findOne: apply optimizations for finding a single page and include pages with 'hidden' status
* @param array $options Options to modify default behavior:
* - `findOne` (bool): apply optimizations for finding a single page and include pages with 'hidden' status
* @return PageArray
* @see Pages::find()
*
*/
public function find($selectorString, $options = array()) {
if(!isset($options['findAll'])) $options['findAll'] = true;
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 */
foreach($pages as $page) {
@@ -257,14 +272,20 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
/**
* Get the first match of your selector string
*
* @param string $selectorString
* @return Page|null
* @param string|int $selectorString
* @return Page|NullPage|null
*
*/
public function get($selectorString) {
$options = $this->getLoadOptions(array('getOne' => true));
if(empty($options['caller'])) {
$caller = $this->className() . ".get($selectorString)";
$options['caller'] = $caller;
} else {
$caller = $options['caller'];
}
if(ctype_digit("$selectorString")) {
// selector string contains a page ID
if(count($this->templates) == 1 && count($this->parents) == 1) {
@@ -292,8 +313,11 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
} else {
// selector string with operators, can pass through
}
$page = $this->pages->get($this->selectorString($selectorString), array('loadOptions' => $options));
$page = $this->pages->get($this->selectorString($selectorString), array(
'caller' => $caller,
'loadOptions' => $options
));
if($page->id && !$this->isValid($page)) $page = $this->wire('pages')->newNullPage();
if($page->id) $this->loaded($page);
@@ -301,13 +325,11 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
}
/**
* Save a page object and it's fields to database.
* Save a page object and its fields to database.
*
* If the page is new, it will be inserted. If existing, it will be updated.
*
* This is the same as calling $page->save()
*
* If you want to just save a particular field in a Page, use $page->save($fieldName) instead.
* - 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.
*
* @param Page $page
* @return bool True on success
@@ -320,11 +342,11 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
}
/**
* Permanently delete a page and it's fields.
* Permanently delete a page and its fields.
*
* Unlike trash(), pages deleted here are not restorable.
* Unlike `$pages->trash()`, pages deleted here are not restorable.
*
* If you attempt to delete a page with children, and don't specifically set the $recursive param to True, then
* 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.
*
* @param Page $page
@@ -339,12 +361,12 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
}
/**
* Adds a new page with the given $name and returns the Page
* Adds a new page with the given $name and returns it
*
* If they page has any other fields, they will not be populated, only the name will.
* Returns a NullPage if error, such as a page of this type already existing with the same name.
* - 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.
*
* @param string $name
* @param string $name Name to use for the new page
* @return Page|NullPage
*
*/
@@ -375,32 +397,84 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
* Make it possible to iterate all pages of this type per the \IteratorAggregate interface.
*
* Only recommended for page types that don't contain a lot of pages.
*
* #pw-internal
*
*/
public function getIterator() {
return $this->find("id>0, sort=name");
}
return $this->find("id>0, sort=name", array(
'caller' => $this->className() . '.getIterator()'
));
}
/**
* Get the template used by this type (or first template if there are multiple)
*
* #pw-group-family
*
* @return Template
*
*/
public function getTemplate() {
return $this->template;
}
/**
* Get the templates (plural) used by this type
*
* #pw-group-family
*
* @return array|Template[] Array of Template objects indexed by template ID.
*
*/
public function getTemplates() {
return count($this->templates) ? $this->templates : array($this->template);
}
/**
* Get the parent page ID used by this type (or first parent ID if there are multiple)
*
* #pw-group-family
*
* @return int
*
*/
public function getParentID() {
return $this->parent_id;
}
/**
* Get the parent page IDs used by this type
*
* #pw-group-family
*
* @return array Array of parent page IDs (integers)
*
*/
public function getParentIDs() {
return count($this->parents) ? $this->parents : array($this->parent_id);
}
/**
* Get the parent Page object (or first parent Page object if there are multiple)
*
* #pw-group-family
*
* @return Page|NullPage
*
*/
public function getParent() {
return $this->wire('pages')->get($this->parent_id);
}
/**
* Get the parent Page objects in a PageArray
*
* #pw-group-family
*
* @return PageArray
*
*/
public function getParents() {
if(count($this->parents)) {
return $this->wire('pages')->getById($this->parents);
@@ -411,17 +485,42 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
return $parents;
}
}
/**
* Set the PHP class name to use for Page objects of this type
*
* #pw-group-family
*
* @param string $class
*
*/
public function setPageClass($class) {
$this->pageClass = $class;
}
/**
* Get the PHP class name used by Page objects of this type
*
* #pw-group-family
*
* @return string
*
*/
public function getPageClass() {
if($this->pageClass) return $this->pageClass;
if($this->template && $this->template->pageClass) return $this->template->pageClass;
return 'Page';
}
/**
* Return the number of pages in this type matching the given selector string
*
* @param string $selectorString Optional, if omitted then returns count of all pages of this type
* @param array $options Options to modify default behavior (see $pages->count method for details)
* @return int
* @see Pages::count()
*
*/
public function count($selectorString = '', array $options = array()) {
if(empty($selectorString) && empty($options) && count($this->parents) == 1) {
return $this->getParent()->numChildren();
@@ -449,9 +548,12 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
/**
* Hook called just before a page is saved
*
* #pw-internal
*
* @param Page $page The page about to be saved
* @return array Optional extra data to add to pages save query.
* @deprecated
*
*/
public function ___saveReady(Page $page) {
@@ -464,32 +566,43 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
*
* This is the same as Pages::save, except that it occurs before other save-related hooks (below),
* Whereas Pages::save occurs after. In most cases, the distinction does not matter.
*
* #pw-internal
*
* @param Page $page The page that was saved
* @param array $changes Array of field names that changed
* @param array $values Array of values that changed, if values were being recorded, see Wire::getChanges(true) for details.
* @deprecated
*
*/
public function ___saved(Page $page, array $changes = array(), $values = array()) { }
/**
* Hook called when a new page has been added
*
* #pw-internal
*
* @param Page $page
* @deprecated
*
*/
public function ___added(Page $page) { }
/**
* Hook called when a page is about to be deleted, but before data has been touched
*
* #pw-internal
*
* @param Page $page
* @deprecated
*
*/
public function ___deleteReady(Page $page) { }
/**
* Hook called when a page and it's data have been deleted
* Hook called when a page and its data have been deleted
*
* #pw-internal
*
* @param Page $page
*

View File

@@ -203,7 +203,7 @@ class PaginatedArray extends WireArray implements WirePaginatable {
$pageNum = $start ? ($start / $limit) + 1 : 1;
$totalPages = ceil($total / $limit);
if(!$totalPages) $pageNum = 0;
$str = sprintf($this->_('%1$s %1$d of %2$d'), $label, $pageNum, $totalPages); // Page quantity, i.e. Page 1 of 3
$str = sprintf($this->_('%1$s %2$d of %3$d'), $label, $pageNum, $totalPages); // Page quantity, i.e. Page 1 of 3
} else {

View File

@@ -164,67 +164,153 @@ class Password extends Wire {
* Modified for camelCase, variable names, and function-based context by Ryan.
*
* @param int $requiredLength Length of string you want returned (default=22)
* @param bool $fast Set to true for a faster, though less random string (default=false, only use true for non-password use)
* @return string
* @param array|bool $options Specify array of options or boolean to specify only `fast` option.
* - `fast` (bool): Use fastest, not cryptographically secure method (default=false).
* - `test` (bool|array): Return tests in a string (bool true), or specify array(true) to return tests array (default=false).
* Note that if the test option is used, then the fast option is disabled.
* @return string|array Returns only array if you specify array for $test argument, otherwise returns string
*
*/
public function randomBase64String($requiredLength = 22, $fast = false) {
public function randomBase64String($requiredLength = 22, $options = array()) {
$defaults = array(
'fast' => false,
'test' => false,
);
if(is_array($options)) {
$options = array_merge($defaults, $options);
} else {
if(is_bool($options)) $defaults['fast'] = $options;
$options = $defaults;
}
$buffer = '';
$valid = false;
$tests = array();
$test = $options['test'];
if($fast) {
if($options['fast'] && !$test) {
// fast mode for non-password use, uses only mt_rand() generated characters
$rawLength = $requiredLength;
} else {
// for password use, slower
$rawLength = (int) ($requiredLength * 3 / 4 + 1);
if(function_exists('mcrypt_create_iv')) {
// mcrypt_create_iv
if((!$valid || $test) && function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
// @operator added for PHP 7.1 which throws deprecated notice on this function call
$buffer = @mcrypt_create_iv($rawLength, MCRYPT_DEV_URANDOM);
if($buffer) $valid = true;
if($test) $tests['mcrypt_create_iv'] = $buffer;
} else if($test) {
$tests['mcrypt_create_iv'] = '';
}
if(!$valid && function_exists('openssl_random_pseudo_bytes')) {
$buffer = openssl_random_pseudo_bytes($rawLength);
if($buffer) $valid = true;
}
if(!$valid && file_exists('/dev/urandom')) {
$f = @fopen('/dev/urandom', 'r');
if($f) {
$read = strlen($buffer);
while($read < $rawLength) {
$buffer .= fread($f, $rawLength - $read);
$read = strlen($buffer);
}
fclose($f);
if($read >= $rawLength) $valid = true;
// PHP7 random_bytes
if((!$valid || $test) && function_exists('random_bytes')) {
try {
$buffer = random_bytes($rawLength);
if($buffer) $valid = true;
} catch(\Exception $e) {
$valid = false;
}
if($test) $tests['random_bytes'] = $buffer;
} else if($test) {
$tests['random_bytes'] = '';
}
// openssl_random_pseudo_bytes
if((!$valid || $test) && function_exists('openssl_random_pseudo_bytes')) {
$good = false;
$buffer = openssl_random_pseudo_bytes($rawLength, $good);
if($test) $tests['openssl_random_pseudo_bytes'] = $buffer . "\tNOTE=" . ($good ? 'strong' : 'NOT strong');
if(!$good) $buffer = '';
if($buffer) $valid = true;
} else if($test) {
$tests['openssl_random_pseudo_bytes'] = '';
}
// read from /dev/urandom
if((!$valid || $test) && @is_readable('/dev/urandom')) {
$f = fopen('/dev/urandom', 'r');
$readLength = 0;
if($test) $buffer = '';
while($readLength < $rawLength) {
$buffer .= fread($f, $rawLength - $readLength);
$readLength = $this->_strlen($buffer);
}
fclose($f);
if($readLength >= $rawLength) $valid = true;
if($test) $tests['/dev/urandom'] = $buffer;
} else if($test) {
$tests['/dev/urandom'] = '';
}
}
if(!$valid || strlen($buffer) < $rawLength) {
$bl = strlen($buffer);
$bufferLength = $this->_strlen($buffer);
// mt_rand() fast
if(!$valid || $test || $bufferLength < $rawLength) {
for($i = 0; $i < $rawLength; $i++) {
if($i < $bl) {
if($i < $bufferLength) {
$buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
} else {
$buffer .= chr(mt_rand(0, 255));
}
}
if($test) $tests['mt_rand'] = $buffer;
}
if($test) {
// test mode
$salt = '';
foreach($tests as $name => $value) {
$note = '';
if(strpos($value, "\tNOTE=")) list($value, $note) = explode("\tNOTE=", $value);
$value = empty($value) ? 'N/A' : $this->randomBufferToSalt($value, $requiredLength);
$_name = str_pad($name, 28, ' ', STR_PAD_LEFT);
$tests[$name] = $value;
$salt .= "\n$_name: $value $note";
}
$salt = is_array($test) ? $tests : ltrim($salt, "\n");
} else {
// regular random string mode
$salt = $this->randomBufferToSalt($buffer, $requiredLength);
}
$salt = str_replace('+', '.', base64_encode($buffer));
$salt = substr($salt, 0, $requiredLength);
//$salt .= $valid; // @todo: what was the point of this?j
return $salt;
}
/**
* Given random buffer string of bytes return base64 encoded salt
*
* @param string $buffer
* @param int $requiredLength
* @return string
*
*/
protected function randomBufferToSalt($buffer, $requiredLength) {
$c1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; // base64
$c2 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; // bcrypt64
$salt = rtrim(base64_encode($buffer), '=');
$salt = strtr($salt, $c1, $c2);
$salt = substr($salt, 0, $requiredLength);
return $salt;
}
/**
* Return string length, using mb_strlen() when available, or strlen() when not
*
* @param string $s
* @return int
*
*/
function _strlen($s) {
return function_exists('mb_strlen') ? mb_strlen($s, '8bit') : strlen($s);
}
/**
* Returns whether the given string is blowfish hashed
*

View File

@@ -113,7 +113,10 @@ class Paths extends WireData {
public function get($key) {
static $_http = null;
if($key == 'root') return $this->_root;
if(strpos($key, 'http') === 0) {
$http = '';
if(is_object($key)) {
$key = "$key";
} else if(strpos($key, 'http') === 0) {
if(is_null($_http)) {
$scheme = $this->wire('input')->scheme;
if(!$scheme) $scheme = 'http';
@@ -123,8 +126,6 @@ class Paths extends WireData {
$http = $_http;
$key = substr($key, 4);
$key[0] = strtolower($key[0]);
} else {
$http = '';
}
if($key == 'root') {
$value = $http . $this->_root;

View File

@@ -125,7 +125,8 @@ abstract class Process extends WireData implements Module {
*
* When any execute() method returns a string, it us used as the actual output.
* When the method returns an associative array, it is considered an array of variables
* to send to the output view layer.
* to send to the output view layer. Returned array must not be empty, otherwise it cannot
* be identified as an associative array.
*
* This execute() method is called when no URL segments are present. You may have any
* number of execute() methods, i.e. `executeFoo()` would be called for the URL `./foo/`
@@ -134,7 +135,10 @@ abstract class Process extends WireData implements Module {
* @return string|array
*
*/
public function ___execute() { }
public function ___execute() {
return ''; // if returning output directly
// return array('name' => 'value'); // if populating a view
}
/**
* Hookable method automatically called after execute() method has finished.
@@ -188,7 +192,7 @@ abstract class Process extends WireData implements Module {
* ~~~~~
*
* @param string $headline
* @return this
* @return $this
*
*/
public function ___headline($headline) {
@@ -204,7 +208,7 @@ abstract class Process extends WireData implements Module {
* ~~~~~
*
* @param string $title
* @return this
* @return $this
*
*/
public function ___browserTitle($title) {
@@ -221,7 +225,7 @@ abstract class Process extends WireData implements Module {
*
* @param string $href URL of breadcrumb
* @param string $label Label for breadcrumb
* @return this
* @return $this
*
*/
public function ___breadcrumb($href, $label) {
@@ -299,8 +303,8 @@ abstract class Process extends WireData implements Module {
*
* #pw-group-module-interface
*
* @param $fromVersion Previous version
* @param $toVersion New version
* @param int|string $fromVersion Previous version
* @param int|string $toVersion New version
* @throws WireException if upgrade fails
*
*/
@@ -493,10 +497,15 @@ abstract class Process extends WireData implements Module {
*
*/
public function setViewFile($file) {
$path = $this->wire('config')->paths . $this->className();
if(strpos($file, $path) !== 0) $file = $path . ltrim($file, '/');
if(strpos($file, '..') !== false) throw new WireException("Invalid view file");
if(!is_file($file)) throw new WireException("View file $file does not exist");
if(strpos($file, '..') !== false) throw new WireException("Invalid view file (relative paths not allowed)");
$config = $this->wire('config');
if(strpos($file, $config->paths->root) === 0 && is_file($file)) {
// full path filename already specified, nothing to auto-determine
} else {
$path = $config->paths($this->className());
if($path && strpos($file, $path) !== 0) $file = $path . ltrim($file, '/\\');
if(!is_file($file)) throw new WireException("View file '$file' does not exist");
}
$this->_viewFile = $file;
return $this;
}

View File

@@ -52,7 +52,15 @@ class ProcessController extends Wire {
* @var string
*
*/
protected $processName;
protected $processName;
/**
* Error message if unable to load Process module
*
* @var string
*
*/
protected $processError = '';
/**
* The name of the method to execute in this process
@@ -141,9 +149,13 @@ class ProcessController extends Wire {
*/
public function getProcess() {
if($this->process) $processName = $this->process->className();
else if($this->processName) $processName = $this->processName;
else return null;
if($this->process) {
$processName = $this->process->className();
} else if($this->processName) {
$processName = $this->processName;
} else {
return null;
}
// verify that there is adequate permission to execute the Process
$permissionName = '';
@@ -151,14 +163,21 @@ class ProcessController extends Wire {
if(!empty($info['permission'])) $permissionName = $info['permission'];
$this->hasPermission($permissionName, true); // throws exception if no permission
if(!$this->process) {
$this->process = $this->modules->getModule($processName);
$module = $this->modules->getModule($processName, array('returnError' => true));
if(is_string($module)) {
$this->processError = $module;
$this->process = null;
} else {
$this->process = $module;
}
}
// set a proces fuel, primarily so that certain Processes can determine if they are the root Process
// set a process fuel, primarily so that certain Processes can determine if they are the root Process
// example: PageList when in PageEdit
$this->wire('process', $this->process);
$this->wire('process', $this->process);
return $this->process;
}
@@ -259,7 +278,7 @@ class ProcessController extends Wire {
}
} else {
throw new ProcessController404Exception("The requested process does not exist");
throw new ProcessController404Exception("The requested process does not exist - $this->processError");
}
if(empty($content) || is_bool($content)) {
@@ -267,7 +286,7 @@ class ProcessController extends Wire {
}
if(is_array($content)) {
// array of returned content indicates variables to send to a view
if(count($content)) {
if(count($content) || $this->process->getViewFile()) {
$viewFile = $this->getViewFile($this->process, $method);
if($viewFile) {
// get output from a separate view file

View File

@@ -1,45 +1,105 @@
<?php namespace ProcessWire;
/**
* ProcessWire API Bootstrap
*
* Initializes all the ProcessWire classes and prepares them for API use
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* https://processwire.com
*
* @todo: get language permissions to work with extra actions
*
*/
require_once(__DIR__ . '/boot.php');
/**
* ProcessWire API bootstrap class
* ProcessWire API Bootstrap
*
* Gets ProcessWire's API ready for use
* #pw-summary Represents an instance of ProcessWire connected with a set of API variables.
* #pw-summary-instances Methods for managing ProcessWire instances. Note that most of these methods are static.
* #pw-use-constants
* #pw-use-constructor
* #pw-body =
* This class boots a ProcessWire instance. The current ProcessWire instance is represented by the `$wire` API variable.
* ~~~~~
* // To create a new ProcessWire instance
* $wire = new ProcessWire('/server/path/', 'https://hostname/url/');
* ~~~~~
* #pw-body
*
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
* @method init()
* @method ready()
* @method finished()
*
*/
*
*
*/
class ProcessWire extends Wire {
const versionMajor = 3;
const versionMinor = 0;
const versionRevision = 39;
/**
* Major version number
*
*/
const versionMajor = 3;
/**
* Minor version number
*
*/
const versionMinor = 0;
/**
* Reversion revision number
*
*/
const versionRevision = 96;
/**
* Version suffix string (when applicable)
*
*/
const versionSuffix = '';
const indexVersion = 300; // required version for index.php file (represented by PROCESSWIRE define)
/**
* Minimum required index.php version, represented by the PROCESSWIRE define
*
*/
const indexVersion = 300;
/**
* Minimum required .htaccess file version
*
*/
const htaccessVersion = 300;
const statusBoot = 0; // system is booting
const statusInit = 2; // system and modules are initializing
const statusReady = 4; // system and $page are ready
const statusRender = 8; // $page's template is being rendered
const statusFinished = 16; // request has been delivered
const statusFailed = 1024; // request failed due to exception or 404
/**
* Status when system is booting
*
*/
const statusBoot = 0;
/**
* Status when system and modules are initializing
*
*/
const statusInit = 2;
/**
* Systus when system, $page and API variables are ready
*
*/
const statusReady = 4;
/**
* Status when the current $pages template file is being rendered
*
*/
const statusRender = 8;
/**
* Status when the request has been fully delivered
*
*/
const statusFinished = 16;
/**
* Status when the request failed due to an Exception or 404
*
*/
const statusFailed = 1024;
/**
* Whether debug mode is on or off
@@ -126,7 +186,7 @@ class ProcessWire extends Wire {
if(is_string($config)) $config = self::buildConfig($config, $rootURL);
if(!$config instanceof Config) throw new WireException("No configuration information available");
// this is reset in the $this->config() method based on current debug mode
// this is reset in the $this->setConfig() method based on current debug mode
ini_set('display_errors', true);
error_reporting(E_ALL | E_STRICT);
@@ -145,7 +205,7 @@ class ProcessWire extends Wire {
$this->wire('hooks', new WireHooks($this, $config), true);
$this->shutdown = $this->wire(new WireShutdown());
$this->config($config);
$this->setConfig($config);
$this->load($config);
if($this->getNumInstances() > 1) {
@@ -170,7 +230,7 @@ class ProcessWire extends Wire {
* @param Config $config
*
*/
protected function config(Config $config) {
protected function setConfig(Config $config) {
$this->wire('config', $config, true);
$this->wire($config->paths);
@@ -193,11 +253,13 @@ class ProcessWire extends Wire {
if($config->https === null) {
$config->https = (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on')
|| (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443);
|| (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443)
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https'); // AWS LOAD BALANCER
}
$config->ajax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest');
$config->cli = (!isset($_SERVER['SERVER_SOFTWARE']) && (php_sapi_name() == 'cli' || ($_SERVER['argc'] > 0 && is_numeric($_SERVER['argc']))));
$config->cli = (!isset($_SERVER['SERVER_SOFTWARE']) && (php_sapi_name() == 'cli' || (isset($_SERVER['argc']) && $_SERVER['argc'] > 0 && is_numeric($_SERVER['argc']))));
$config->modal = empty($_GET['modal']) ? false : abs((int) $_GET['modal']);
$version = self::versionMajor . "." . self::versionMinor . "." . self::versionRevision;
$config->version = $version;
@@ -215,13 +277,17 @@ class ProcessWire extends Wire {
// If script is being called externally, add an extra shutdown function
if(!$config->internal) register_shutdown_function(function() {
if(error_get_last()) return;
$process = $this->wire('process');
$process = isset($this) ? $this->wire('process') : wire('process');
if($process == 'ProcessPageView') $process->finished();
});
if($config->useFunctionsAPI) {
include($config->paths->core . 'FunctionsAPI.php');
$file = $config->paths->core . 'FunctionsAPI.php';
/** @noinspection PhpIncludeInspection */
include_once($file);
}
$this->setStatus(self::statusBoot);
}
@@ -277,7 +343,9 @@ class ProcessWire extends Wire {
}
/**
* Load's ProcessWire using the supplied Config and populates all API fuel
* Loads ProcessWire using the supplied Config and populates all API fuel
*
* #pw-internal
*
* @param Config $config
* @throws WireDatabaseException|WireException on fatal error
@@ -290,6 +358,7 @@ class ProcessWire extends Wire {
Debug::timer('boot.load');
}
$this->wire('urls', $config->urls); // shortcut API var
$this->wire('log', new WireLog(), true);
$this->wire('notices', new Notices(), true);
$this->wire('sanitizer', new Sanitizer());
@@ -307,7 +376,8 @@ class ProcessWire extends Wire {
$this->trackException($e, true, 'Unable to load WireDatabasePDO');
throw new WireDatabaseException($e->getMessage());
}
/** @var WireCache $cache */
$cache = $this->wire('cache', new WireCache(), true);
$cache->preload($config->preloadCacheNames);
@@ -417,6 +487,8 @@ class ProcessWire extends Wire {
/**
* Hookable init for anyone that wants to hook immediately before any autoload modules initialized or after all modules initialized
*
* #pw-hooker
*
*/
protected function ___init() {
if($this->debug) Debug::timer('boot.modules.autoload.init');
@@ -426,6 +498,8 @@ class ProcessWire extends Wire {
/**
* Hookable ready for anyone that wants to hook immediately before any autoload modules ready or after all modules ready
*
* #pw-hooker
*
*/
protected function ___ready() {
@@ -438,6 +512,8 @@ class ProcessWire extends Wire {
/**
* Hookable ready for anyone that wants to hook when the request is finished
*
* #pw-hooker
*
*/
protected function ___finished() {
@@ -513,7 +589,16 @@ class ProcessWire extends Wire {
if(is_object($value)) return call_user_func_array(array($value, '__invoke'), $arguments);
return parent::__call($method, $arguments);
}
/**
* Get an API variable
*
* #pw-internal
*
* @param string $name Optional API variable name
* @return mixed|null|Fuel
*
*/
public function fuel($name = '') {
if(empty($name)) return $this->fuel;
return $this->fuel->$name;
@@ -540,6 +625,8 @@ class ProcessWire extends Wire {
/**
* Instance ID of this ProcessWire instance
*
* #pw-group-instances
*
* @return int
*
*/
@@ -550,6 +637,8 @@ class ProcessWire extends Wire {
/**
* Add a ProcessWire instance and return the instance ID
*
* #pw-group-instances
*
* @param ProcessWire $wire
* @return int
*
@@ -564,6 +653,8 @@ class ProcessWire extends Wire {
/**
* Get all ProcessWire instances
*
* #pw-group-instances
*
* @return array
*
*/
@@ -574,6 +665,8 @@ class ProcessWire extends Wire {
/**
* Return number of instances
*
* #pw-group-instances
*
* @return int
*
*/
@@ -584,6 +677,8 @@ class ProcessWire extends Wire {
/**
* Get a ProcessWire instance by ID
*
* #pw-group-instances
*
* @param int|null $instanceID Omit this argument to return the current instance
* @return null|ProcessWire
*
@@ -596,6 +691,8 @@ class ProcessWire extends Wire {
/**
* Get the current ProcessWire instance
*
* #pw-group-instances
*
* @return ProcessWire|null
*
*/
@@ -610,6 +707,8 @@ class ProcessWire extends Wire {
/**
* Set the current ProcessWire instance
*
* #pw-group-instances
*
* @param ProcessWire $wire
*
*/
@@ -620,6 +719,8 @@ class ProcessWire extends Wire {
/**
* Remove a ProcessWire instance
*
* #pw-group-instances
*
* @param ProcessWire $wire
*
*/
@@ -634,7 +735,7 @@ class ProcessWire extends Wire {
}
/**
* Build a Config object for booting ProcessWire
* Static method to build a Config object for booting ProcessWire
*
* @param string $rootPath Path to root of installation where ProcessWire's index.php file is located.
* @param string $rootURL Should be specified only for secondary ProcessWire instances.
@@ -642,27 +743,31 @@ class ProcessWire extends Wire {
* @param array $options Options to modify default behaviors (experimental):
* - `siteDir` (string): Name of "site" directory in $rootPath that contains site's config.php, no slashes (default="site").
* @return Config
* @throws WireException
*
*/
public static function buildConfig($rootPath, $rootURL = null, array $options = array()) {
if(strpos($rootPath, '..') !== false) {
$rootPath = realpath($rootPath);
if($rootPath === false) throw new WireException("Path not found");
}
if(DIRECTORY_SEPARATOR != '/') {
$rootPath = str_replace(DIRECTORY_SEPARATOR, '/', $rootPath);
}
if(strpos($rootPath, '..') !== false) $rootPath = realpath($rootPath);
$httpHost = '';
$scheme = '';
$siteDir = isset($options['siteDir']) ? $options['siteDir'] : 'site';
$cfg = array('dbName' => '');
if($rootURL && strpos($rootURL, '://')) {
// rootURL is specifying scheme and hostname
list($scheme, $httpHost) = explode('://', $rootURL);
if(strpos($httpHost, '/')) {
list($httpHost, $rootURL) = explode('/', $httpHost, 2);
if(empty($rootURL)) $rootURL = '/';
$rootURL = "/$rootURL";
} else {
$rootURL = '/';
}
@@ -674,16 +779,13 @@ class ProcessWire extends Wire {
$_rootURL = $rootURL;
if(is_null($rootURL)) $rootURL = '/';
$config = new Config();
$config->dbName = '';
// check what rootPath is referring to
if(strpos($rootPath, "/$siteDir")) {
$parts = explode('/', $rootPath);
$testDir = array_pop($parts);
if(($testDir === $siteDir || strpos($testDir, 'site-') === 0) && is_file("$rootPath/config.php")) {
// rootPath was given as a /site/ directory rather than root directory
$rootPath = '/' . implode('/', $parts); // remove siteDir from rootPath
$rootPath = implode('/', $parts); // remove siteDir from rootPath
$siteDir = $testDir; // set proper siteDir
}
}
@@ -706,11 +808,11 @@ class ProcessWire extends Wire {
unset($sf, $f, $x);
// when internal is true, we are not being called by an external script
$config->internal = $realIndexFile == $realScriptFile;
$cfg['internal'] = strtolower($realIndexFile) == strtolower($realScriptFile);
} else {
// when included from another app or command line script
$config->internal = false;
$cfg['internal'] = false;
$host = '';
}
@@ -737,42 +839,50 @@ class ProcessWire extends Wire {
}
// other default directories
$sitePath = $rootPath . "/$siteDir/";
$wireDir = "wire";
$coreDir = "$wireDir/core";
$assetsDir = "$siteDir/assets";
$adminTplDir = 'templates-admin';
// create new Config instance
$config->urls = new Paths($rootURL);
$config->urls->wire = "$wireDir/";
$config->urls->site = "$siteDir/";
$config->urls->modules = "$wireDir/modules/";
$config->urls->siteModules = "$siteDir/modules/";
$config->urls->core = "$coreDir/";
$config->urls->assets = "$assetsDir/";
$config->urls->cache = "$assetsDir/cache/";
$config->urls->logs = "$assetsDir/logs/";
$config->urls->files = "$assetsDir/files/";
$config->urls->tmp = "$assetsDir/tmp/";
$config->urls->templates = "$siteDir/templates/";
$config->urls->fieldTemplates = "$siteDir/templates/fields/";
$config->urls->adminTemplates = is_dir("$siteDir/$adminTplDir") ? "$siteDir/$adminTplDir/" : "$wireDir/$adminTplDir/";
$config->paths = clone $config->urls;
$config->paths->root = $rootPath . '/';
$config->paths->sessions = $config->paths->assets . "sessions/";
$cfg['urls'] = new Paths($rootURL);
$cfg['urls']->data(array(
'wire' => "$wireDir/",
'site' => "$siteDir/",
'modules' => "$wireDir/modules/",
'siteModules' => "$siteDir/modules/",
'core' => "$coreDir/",
'assets' => "$assetsDir/",
'cache' => "$assetsDir/cache/",
'logs' => "$assetsDir/logs/",
'files' => "$assetsDir/files/",
'tmp' => "$assetsDir/tmp/",
'templates' => "$siteDir/templates/",
'fieldTemplates' => "$siteDir/templates/fields/",
'adminTemplates' => "$wireDir/$adminTplDir/",
), true);
$cfg['paths'] = clone $cfg['urls'];
$cfg['paths']->set('root', $rootPath . '/');
$cfg['paths']->data('sessions', $cfg['paths']->assets . "sessions/");
// Styles and scripts are CSS and JS files, as used by the admin application.
// But reserved here if needed by other apps and templates.
$config->styles = new FilenameArray();
$config->scripts = new FilenameArray();
$cfg['styles'] = new FilenameArray();
$cfg['scripts'] = new FilenameArray();
$config = new Config();
$config->setTrackChanges(false);
$config->data($cfg, true);
// Include system config defaults
/** @noinspection PhpIncludeInspection */
require("$rootPath/$wireDir/config.php");
// Include site-specific config settings
$configFile = $config->paths->site . "config.php";
$configFileDev = $config->paths->site . "config-dev.php";
$configFile = $sitePath . "config.php";
$configFileDev = $sitePath . "config-dev.php";
if(is_file($configFileDev)) {
/** @noinspection PhpIncludeInspection */
@require($configFileDev);
@@ -785,8 +895,9 @@ class ProcessWire extends Wire {
$config->httpHost = $httpHost;
if(!in_array($httpHost, $config->httpHosts)) $config->httpHosts[] = $httpHost;
}
if($scheme) $config->https = ($scheme === 'https');
return $config;
}

View File

@@ -24,6 +24,8 @@ class Role extends Page {
/**
* Create a new Role page in memory.
*
* @param Template $tpl
*
*/
public function __construct(Template $tpl = null) {

View File

@@ -28,7 +28,7 @@ class Roles extends PagesType {
*
* #pw-internal
*
* @return Role
* @return Role|NullPage|Page
* @throws WireException
*
*/
@@ -84,8 +84,8 @@ class Roles extends PagesType {
*
* #pw-group-manipulation
*
* @param string $name Name of permission you want to add, i.e. "hello-world"
* @return Role|Page|NullPage Returns a Permission page on success, or a NullPage on error
* @param string $name Name of role you want to add, i.e. "hello-world"
* @return Role|Page|NullPage Returns a Role page on success, or a NullPage on error
*
*/
public function ___add($name) {
@@ -96,6 +96,8 @@ class Roles extends PagesType {
* Ensure that every role has at least 'page-view' permission
*
* #pw-internal
*
* @param Page $page
*
*/
protected function loaded(Page $page) {
@@ -103,4 +105,23 @@ class Roles extends PagesType {
$page->permissions->add($this->wire('permissions')->get("name=page-view"));
}
}
/**
* Hook called when a page and its data have been deleted
*
* #pw-internal
*
* @param Page $page
*
*/
public function ___deleted(Page $page) {
foreach($this->wire('templates') as $template) {
/** @var Template $template */
if(!$template->useRoles) continue;
$template->removeRole($page, 'all');
if($template->isChanged()) $template->save();
}
parent::___deleted($page);
}
}

View File

@@ -68,7 +68,7 @@ class Sanitizer extends Wire {
*
*/
public function __construct() {
$this->multibyteSupport = function_exists("mb_strlen");
$this->multibyteSupport = function_exists("mb_internal_encoding");
if($this->multibyteSupport) mb_internal_encoding("UTF-8");
$this->allowedASCII = str_split('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
}
@@ -116,8 +116,10 @@ class Sanitizer extends Wire {
}
}
$v = iconv("UTF-8", "ASCII//TRANSLIT//IGNORE", $value);
if($v) $value = $v;
if(function_exists("\\iconv")) {
$v = iconv("UTF-8", "ASCII//TRANSLIT//IGNORE", $value);
if($v) $value = $v;
}
$needsWork = strlen(str_replace($allowed, '', $value));
}
@@ -447,15 +449,16 @@ class Sanitizer extends Wire {
* #pw-group-pages
*
* @param string $value Value to sanitize
* @param int $maxLength Maximum number of characters allowed
* @return string Sanitized value
*
*/
public function pageNameUTF8($value) {
public function pageNameUTF8($value, $maxLength = 128) {
if(!strlen($value)) return '';
// if UTF8 module is not enabled then delegate this call to regular pageName sanitizer
if($this->wire('config')->pageNameCharset != 'UTF8') return $this->pageName($value);
if($this->wire('config')->pageNameCharset != 'UTF8') return $this->pageName($value, false, $maxLength);
// we don't allow UTF8 page names to be prefixed with "xn-"
if(strpos($value, 'xn-') === 0) $value = substr($value, 3);
@@ -467,7 +470,7 @@ class Sanitizer extends Wire {
$extras = array('.', '-', '_', ' ', ',', ';', ':', '(', ')', '!', '?', '&', '%', '$', '#', '@');
// proceed only if value has some non-ascii characters
if(ctype_alnum(str_replace($extras, '', $value))) return $this->pageName($value);
if(ctype_alnum(str_replace($extras, '', $value))) return $this->pageName($value, false, $maxLength);
// validate that all characters are in our whitelist
$whitelist = $this->wire('config')->pageNameWhitelist;
@@ -513,6 +516,8 @@ class Sanitizer extends Wire {
// trim off any remaining separators/extras
$value = trim($value, '-_.');
if(mb_strlen($value) > $maxLength) $value = mb_substr($value, 0, $maxLength);
return $value;
}
@@ -914,15 +919,16 @@ class Sanitizer extends Wire {
* #pw-group-strings
*
* @param string $value String value to sanitize
* @param array $options Options to modify default behavior:
* - `maxLength` (int): maximum characters allowed, or 0=no max (default=255).
* - `maxBytes` (int): maximum bytes allowed (default=0, which implies maxLength*4).
* - `stripTags` (bool): strip markup tags? (default=true).
* - `allowableTags` (string): markup tags that are allowed, if stripTags is true (use same format as for PHP's `strip_tags()` function.
* - `multiLine` (bool): allow multiple lines? if false, then $newlineReplacement below is applicable (default=false).
* - `newlineReplacement` (string): character to replace newlines with, OR specify boolean TRUE to remove extra lines (default=" ").
* - `inCharset` (string): input character set (default="UTF-8").
* - `outCharset` (string): output character set (default="UTF-8").
* @param array $options Options to modify default behavior:
* - `maxLength` (int): maximum characters allowed, or 0=no max (default=255).
* - `maxBytes` (int): maximum bytes allowed (default=0, which implies maxLength*4).
* - `stripTags` (bool): strip markup tags? (default=true).
* - `stripMB4` (bool): strip emoji and other 4-byte UTF-8? (default=false).
* - `allowableTags` (string): markup tags that are allowed, if stripTags is true (use same format as for PHP's `strip_tags()` function.
* - `multiLine` (bool): allow multiple lines? if false, then $newlineReplacement below is applicable (default=false).
* - `newlineReplacement` (string): character to replace newlines with, OR specify boolean TRUE to remove extra lines (default=" ").
* - `inCharset` (string): input character set (default="UTF-8").
* - `outCharset` (string): output character set (default="UTF-8").
* @return string
* @see Sanitizer::textarea()
*
@@ -933,6 +939,7 @@ class Sanitizer extends Wire {
'maxLength' => 255, // maximum characters allowed, or 0=no max
'maxBytes' => 0, // maximum bytes allowed (0 = default, which is maxLength*4)
'stripTags' => true, // strip markup tags
'stripMB4' => false, // strip Emoji and 4-byte characters?
'allowableTags' => '', // tags that are allowed, if stripTags is true (use same format as for PHP's strip_tags function)
'multiLine' => false, // allow multiple lines? if false, then $newlineReplacement below is applicable
'newlineReplacement' => ' ', // character to replace newlines with, OR specify boolean TRUE to remove extra lines
@@ -941,6 +948,8 @@ class Sanitizer extends Wire {
);
$options = array_merge($defaultOptions, $options);
if($options['maxLength'] < 0) $options['maxLength'] = 0;
if($options['maxBytes'] < 0) $options['maxBytes'] = 0;
if(!is_string($value)) $value = $this->string($value);
@@ -962,7 +971,9 @@ class Sanitizer extends Wire {
if($options['stripTags']) $value = strip_tags($value, $options['allowableTags']);
if($options['inCharset'] != $options['outCharset']) $value = iconv($options['inCharset'], $options['outCharset'], $value);
if($options['inCharset'] != $options['outCharset']) $value = iconv($options['inCharset'], $options['outCharset'], $value);
if($options['stripMB4']) $value = $this->removeMB4($value);
if($options['maxLength']) {
if(empty($options['maxBytes'])) $options['maxBytes'] = $options['maxLength'] * 4;
@@ -1007,9 +1018,10 @@ class Sanitizer extends Wire {
*
* @param string $value String value to sanitize
* @param array $options Options to modify default behavior
* - `maxLength` (int): maximum characters allowed, or 0=no max (default=16384 or 16kb).
* - `maxLength` (int): maximum characters allowed, or 0=no max (default=16384 or 16kb).
* - `maxBytes` (int): maximum bytes allowed (default=0, which implies maxLength*3 or 48kb).
* - `stripTags` (bool): strip markup tags? (default=true).
* - `stripMB4` (bool): strip emoji and other 4-byte UTF-8? (default=false).
* - `allowableTags` (string): markup tags that are allowed, if stripTags is true (use same format as for PHP's `strip_tags()` function.
* - `allowCRLF` (bool): allow CR+LF newlines (i.e. "\r\n")? (default=false, which means "\r\n" is replaced with "\n").
* - `inCharset` (string): input character set (default="UTF-8").
@@ -1070,9 +1082,10 @@ class Sanitizer extends Wire {
if(strpos($value, '<') !== false) {
// tag replacements before strip_tags()
$regex =
'!(?:<' .
'/?(?:ul|ol|p|h\d|div)(?:>|\s[^><]*)' .
'|br[\s/]*' .
'!<(?:' .
'/?(?:ul|ol|p|h\d|div)(?:>|\s[^><]*)' .
'|' .
'(?:br[\s/]*)' .
')>!is';
$value = preg_replace($regex, $newline, $value);
if(stripos($value, '</li>')) {
@@ -1517,7 +1530,8 @@ class Sanitizer extends Wire {
}
$value = trim($value); // trim any kind of whitespace
$value = trim($value, '+!,'); // chars to remove from begin and end
$value = trim($value, '+,'); // chars to remove from begin and end
if(strpos($value, '!') !== false) $needsQuotes = true;
if(!$needsQuotes && $options['useQuotes'] && strlen($value)) {
$a = substr($value, 0, 1);
@@ -1588,14 +1602,13 @@ class Sanitizer extends Wire {
* for text coming from user input since it doesn't allow any other HTML. But if you just
* want full markdown, then specify TRUE for the `$options` argument.
*
* ~~~~~
* Basic allowed markdown currently includes:
* **strong**
* *emphasis*
* [anchor-text](url)
* ~~strikethrough~~
* `code`
* ~~~~~
* - `**strong**`
* - `*emphasis*`
* - `[anchor-text](url)`
* - `~~strikethrough~~`
* - code surrounded by backticks
*
* ~~~~~
* // basic markdown
* echo $sanitizer->entitiesMarkdown($str);
@@ -1721,6 +1734,7 @@ class Sanitizer extends Wire {
* @param int|bool $flags See PHP html_entity_decode function for flags.
* @param string $encoding Encoding (default="UTF-8").
* @return string String with entities removed.
* @see Sanitizer::entities()
*
*/
public function unentities($str, $flags = ENT_QUOTES, $encoding = 'UTF-8') {
@@ -1799,6 +1813,43 @@ class Sanitizer extends Wire {
return str_replace(array("\r\n", "\r", "\n"), $replacement, $str);
}
/**
* Removes 4-byte UTF-8 characters (like emoji) that produce error with with MySQL regular “UTF8” encoding
*
* Returns the same value type that it is given. If given something other than a string or array, it just
* returns it without modification.
*
* #pw-group-strings
*
* @param string|array $value String or array containing strings
* @return string|array|mixed
*
*/
function removeMB4($value) {
if(empty($value)) return $value;
if(is_array($value)) {
// process array recursively, looking for strings to convert
foreach($value as $key => $val) {
if(empty($val)) continue;
if(is_string($val) || is_array($val)) $value[$key] = $this->removeMB4($val);
}
} else if(is_string($value)) {
if(strlen($value) > 3 && max(array_map('ord', str_split($value))) >= 240) {
// string contains 4-byte characters
$regex =
'!(?:' .
'\xF0[\x90-\xBF][\x80-\xBF]{2}' .
'|[\xF1-\xF3][\x80-\xBF]{3}' .
'|\xF4[\x80-\x8F][\x80-\xBF]{2}' .
')!s';
$value = preg_replace($regex, '', $value);
}
} else {
// not a string or an array, leave as-is
}
return $value;
}
/**
* Sanitize value to string
*
@@ -1823,9 +1874,11 @@ class Sanitizer extends Wire {
$value = "";
} else if(is_bool($value)) {
$value = $value ? "1" : "";
} else if(is_array($value)) {
$value = "array-" . count($value);
} else if(!is_string($value)) {
$value = (string) $value;
}
if(is_array($value)) $value = "array-" . count($value);
if(!is_string($value)) $value = (string) $value;
if(!is_null($sanitizer) && is_string($sanitizer) && (method_exists($this, $sanitizer) || method_exists($this, "___$sanitizer"))) {
$value = $this->$sanitizer($value);
if(!is_string($value)) $value = (string) $value;
@@ -1837,8 +1890,9 @@ class Sanitizer extends Wire {
* Sanitize a date or date/time string, making sure it is valid, and return it
*
* - If no date $format is specified, date will be returned as a unix timestamp.
* - If given date is invalid or empty, NULL will be returned.
* - If given date in invalid format and cant be made valid, or date is empty, NULL will be returned.
* - If $value is an integer or string of all numbers, it is always assumed to be a unix timestamp.
* - If $format and “strict” option specified, date will also validate for format and no out-of-bounds values will be converted.
*
* #pw-group-strings
* #pw-group-numbers
@@ -1850,6 +1904,7 @@ class Sanitizer extends Wire {
* - `min` (string|int): Minimum allowed date in $format or unix timestamp format. Null is returned when date is less than this.
* - `max` (string|int): Maximum allowed date in $format or unix timestamp format. Null is returned when date is more than this.
* - `default` (mixed): Default value to return if no value specified.
* - `strict` (bool): Force dates that dont match given $format, or out of bounds, to fail. Requires $format. (default=false)
* @return string|int|null
*
*/
@@ -1859,8 +1914,11 @@ class Sanitizer extends Wire {
'min' => '', // Minimum date allowed (in $dateFormat format, or a unix timestamp)
'max' => '', // Maximum date allowed (in $dateFormat format, or a unix timestamp)
'default' => null, // Default value, if date didn't resolve
'strict' => false,
);
$options = array_merge($defaults, $options);
$datetime = $this->wire('datetime');
$_value = trim($value); // original value string
if(empty($value)) return $options['default'];
if(!is_string($value) && !is_int($value)) $value = $this->string($value);
if(ctype_digit("$value")) {
@@ -1868,10 +1926,16 @@ class Sanitizer extends Wire {
// make sure it resolves to a valid date
$value = strtotime(date('Y-m-d H:i:s', (int) $value));
} else {
$value = strtotime($value);
/** @var WireDateTime $datetime */
$value = $datetime->stringToTimestamp($value, $format);
}
// value is now a unix timestamp
if(empty($value)) return null;
// if format is provided and in strict mode, validate for the format and bounds
if($format && $options['strict']) {
$test = $datetime->date($format, $value);
if($test !== $_value) return null;
}
if(!empty($options['min'])) {
// if value is less than minimum required, return null/error
$min = ctype_digit("$options[min]") ? (int) $options['min'] : (int) wireDate('ts', $options['min']);
@@ -2178,10 +2242,11 @@ class Sanitizer extends Wire {
* #pw-group-arrays
*
* @param array $data Array to reduce
* @param bool|array $allowEmpty Should empty values be allowed in the encoded data?
* @param bool|array $allowEmpty Should empty values be allowed in the encoded data? Specify any of the following:
* - `false` (bool): to exclude all empty values (this is the default if not specified).
* - `true` (bool): to allow all empty values to be retained (thus no point in calling this function).
* - Specify array of keys (from data) that should be retained if you want some retained and not others.
* - Specify array of literal empty value types to retain, i.e. [ 0, '0', array(), false, null ]
* - Specify the digit `0` to retain values that are 0, but not other types of empty values.
* @param bool $convert Perform type conversions where appropriate? i.e. convert digit-only string to integer (default=false).
* @return array
@@ -2192,6 +2257,14 @@ class Sanitizer extends Wire {
if(!is_array($data)) {
$data = $this->___array($data, null);
}
$allowEmptyTypes = array();
if(is_array($allowEmpty)) {
foreach($allowEmpty as $emptyType) {
if(!empty($emptyType)) continue;
$allowEmptyTypes[] = $emptyType;
}
}
foreach($data as $key => $value) {
@@ -2208,21 +2281,33 @@ class Sanitizer extends Wire {
$data[$key] = $value;
// skip empty values whether blank, 0, empty array, etc.
if(empty($value)) {
if($allowEmpty === 0 && $value === 0) {
// keep it because $allowEmpty === 0 means to keep 0 values only
} else if(is_array($allowEmpty) && !in_array($key, $allowEmpty)) {
// remove it because it's not specifically allowed in allowEmpty
unset($data[$key]);
} else if(!$allowEmpty) {
// remove the empty value
unset($data[$key]);
// if value is not empty, no need to continue further checks
if(!empty($value)) continue;
$typeMatched = false;
if(count($allowEmptyTypes)) {
foreach($allowEmptyTypes as $emptyType) {
if($value === $emptyType) {
$typeMatched = true;
break;
}
}
}
if($typeMatched) {
// keep it because type matched an allowEmptyTypes
} else if($allowEmpty === 0 && $value === 0) {
// keep it because $allowEmpty === 0 means to keep 0 values only
} else if(is_array($allowEmpty) && !in_array($key, $allowEmpty)) {
// remove it because it's not specifically allowed in allowEmpty
unset($data[$key]);
} else if(!$allowEmpty) {
// remove the empty value
unset($data[$key]);
}
}
return $data;
@@ -2314,42 +2399,50 @@ class Sanitizer extends Wire {
*/
public function testAll($value) {
$sanitizers = array(
'alpha',
'alphanumeric',
'array',
'bool',
'date',
'digits',
'email',
'emailHeader',
'entities',
'entities1',
'entitiesMarkdown',
'fieldName',
'filename',
'float',
'int',
'intArray',
'intSigned',
'intUnsigned',
'markupToLine',
'markupToText',
'minArray',
'name',
'names',
'varName',
'fieldName',
'templateName',
'pageName',
'pageNameTranslate',
'pageNameUTF8',
'filename',
'path',
'pagePathName',
'email',
'emailHeader',
'text',
'textarea',
'url',
'pagePathNameUTF8',
'path',
'purify',
'removeNewlines',
'selectorField',
'selectorValue',
'entities',
'entities1',
'unentities',
'entitiesMarkdown',
'purify',
'string',
'date',
'int',
'intUnsigned',
'intSigned',
'float',
'array',
'intArray',
'bool',
'templateName',
'text',
'textarea',
'unentities',
'url',
'varName',
);
$results = array();
foreach($sanitizers as $method) {
$results[$method] = $this->$method($value);
$results[$method] = $this->$method($value);
}
return $results;
}

View File

@@ -11,33 +11,62 @@
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* https://processwire.com
*
*/
/**
* Selector maintains a single selector consisting of field name, operator, and value.
*
* Field and value may optionally be arrays, where are assumed to be OR values.
* #pw-summary Selector maintains a single selector consisting of field name, operator, and value.
*
* Serves as the base class for the different Selector types (seen below this class).
* #pw-body =
* - Serves as the base class for the different Selector types (`SelectorEqual`, `SelectorNotEqual`, `SelectorLessThan`, etc.)
* - The constructor requires `$field` and `$value` properties which may either be an array or string.
* An array indicates multiple items in an OR condition. Multiple items may also be specified by
* pipe “|” separated strings.
* - Operator is determined by the Selector class name, and thus may not be changed without replacing
* the entire Selector.
*
* @property string|array $field Field or fields present in the selector (can be string or array) [1]
* @property array $fields Fields that were present in selector (same as $field, but always array)
* @property string $operator Operator used by the selector [2]
* @property string|array $value Value or values present in the selector (can be string or array) [1]
* @property array $values Values that were present in selector (same as $value, but always array)
* @property bool $not Is this a NOT selector? (i.e. returns the opposite if what it would otherwise)
* @property string|null $group Group name for this selector (if field was prepended with a "group_name@")
* @property string $quote Type of quotes value was in, or blank if it was not quoted. One of: '"[{(
* @property string $str String value of selector
* @property null|bool $forceMatch When boolean, it forces match (true) or non-match (false).
* ~~~~~
* // very basic usage example
* // constructor takes ($field, $value) which can be strings or arrays
* $s = new SelectorEqual('title', 'About Us');
* // $page can be any kind of Wire-derived object
* if($s->matches($page)) {
* // $page has title "About Us"
* }
* ~~~~~
* ~~~~~
* // another usage example
* $s = new SelectorContains('title|body|summary', 'foo|bar');
* if($s->matches($page)) {
* // the title, body or summary properties of $page contain either the text "foo" or "bar"
* }
* ~~~~~
*
* [1] The $field and $value properties may either be an array or string. As a result, we recommend
* accessing the $fields or $values properties (instead of $field or $value), because they are always
* return an array.
* ### List of core selector-derived classes
*
* [2] Operator is determined by the Selector class name, and thus may not be changed without replacing
* the entire Selector.
* - `SelectorEqual`
* - `SelectorNotEqual`
* - `SelectorGreaterThan`
* - `SelectorLessThan`
* - `SelectorGreaterThanEqual`
* - `SelectorLessThanEqual`
* - `SelectorContains`
* - `SelectorContainsLike`
* - `SelectorContainsWords`
* - `SelectorStarts`
* - `SelectorStartsLike`
* - `SelectorEnds`
* - `SelectorEndsLike`
* - `SelectorBitwiseAnd`
*
* #pw-body
*
* @property array $fields Fields that were present in selector (same as $field, but always an array).
* @property string|array $field Field or fields present in the selector (string if single, or array of strings if multiple). Preferable to use $fields property instead.
* @property-read string $operator Operator used by the selector.
* @property array $values Values that were present in selector (same as $value, but always array).
* @property string|array $value Value or values present in the selector (string if single, or array of strings if multiple). Preferable to use $values property instead.
* @property bool $not Is this a NOT selector? Indicates the selector returns the opposite if what it would otherwise. #pw-group-properties
* @property string|null $group Group name for this selector (if field was prepended with a "group_name@"). #pw-group-properties
* @property string $quote Type of quotes value was in, or blank if it was not quoted. One of: '"[{( #pw-group-properties
* @property-read string $str String value of selector, i.e. “a=b”. #pw-group-properties
* @property null|bool $forceMatch When boolean, it forces match (true) or non-match (false). (default=null) #pw-group-properties
*
*/
abstract class Selector extends WireData {
@@ -71,27 +100,130 @@ abstract class Selector extends WireData {
$this->set('forceMatch', null); // boolean true to force match, false to force non-match
}
/**
* Return the operator used by this Selector
*
* @return string
* @since 3.0.42 Prior versions just supported the 'operator' property.
*
*/
public function operator() {
return static::getOperator();
}
/**
* Get the field(s) of this Selector
*
* Note that if calling this as a property (rather than a method) it can return either a string or an array.
*
* @param bool|int $forceString Specify one of the following:
* - `true` (bool): to only return a string, where multiple-fields will be split by pipe "|". (default)
* - `false` (bool): to return string if 1 field, or array of multiple fields (same behavior as field property).
* - `1` (int): to return only the first value (string).
* @return string|array|null
* @since 3.0.42 Prior versions only supported the 'field' property.
* @see Selector::fields()
*
*/
public function field($forceString = true) {
$field = parent::get('field');
if($forceString && is_array($field)) {
if($forceString === 1) {
$field = reset($field);
} else {
$field = implode('|', $field);
}
}
return $field;
}
/**
* Return array of field(s) for this Selector
*
* @return array
* @see Selector::field()
* @since 3.0.42 Prior versions just supported the 'fields' property.
*
*/
public function fields() {
$field = parent::get('field');
if(is_array($field)) return $field;
if(!strlen($field)) return array();
return array($field);
}
/**
* Get the value(s) of this Selector
*
* Note that if calling this as a property (rather than a method) it can return either a string or an array.
*
* @param bool|int $forceString Specify one of the following:
* - `true` (bool): to only return a string, where multiple-values will be split by pipe "|". (default)
* - `false` (bool): to return string if 1 value, or array of multiple values (same behavior as value property).
* - `1` (int): to return only the first value (string).
* @return string|array|null
* @since 3.0.42 Prior versions only supported the 'value' property.
* @see Selector::values()
*
*/
public function value($forceString = true) {
$value = parent::get('value');
if($forceString && is_array($value)) {
if($forceString === 1) {
$value = reset($value);
} else {
$value = implode('|', $value);
}
}
return $value;
}
/**
* Return array of value(s) for this Selector
*
* @param bool $nonEmpty If empty array will be returned, forces it to return array with one blank item instead (default=false).
* @return array
* @see Selector::value()
* @since 3.0.42 Prior versions just supported the 'values' property.
*
*/
public function values($nonEmpty = false) {
$values = parent::get('value');
if(is_array($values)) {
// ok
} else if(is_string($values)) {
$values = strlen($values) ? array($values) : array();
} else if(is_object($values)) {
$values = $values instanceof WireArray ? $values->getArray() : array($values);
} else if($values) {
$values = array($values);
} else {
$values = array();
}
if($nonEmpty && !count($values)) $values = array('');
return $values;
}
/**
* Get a property
*
* @param string $key Property name
* @return array|mixed|null|string Property value
*
*/
public function get($key) {
if($key == 'operator') return $this->getOperator();
if($key == 'operator') return $this->operator();
if($key == 'str') return $this->__toString();
if($key == 'values') {
$value = $this->value;
if(is_array($value)) return $value;
if(!is_object($value) && !strlen($value)) return array();
return array($value);
}
if($key == 'fields') {
$field = $this->field;
if(is_array($field)) return $field;
if(!strlen($field)) return array();
return array($field);
}
if($key == 'values') return $this->values();
if($key == 'fields') return $this->fields();
return parent::get($key);
}
/**
* Returns the selector field(s), optionally forcing as string or array
*
* #pw-internal
*
* @param string $type Omit for automatic, or specify 'string' or 'array' to force return in that type
* @return string|array
* @throws WireException if given invalid type
@@ -115,6 +247,8 @@ abstract class Selector extends WireData {
* When the $type argument is not specified, this method may return a string, array or Selectors object.
* A Selectors object is only returned if the value happens to contain an embedded selector.
*
* #pw-internal
*
* @param string $type Omit for automatic, or specify 'string' or 'array' to force return in that type
* @return string|array|Selectors
* @throws WireException if given invalid type
@@ -143,7 +277,7 @@ abstract class Selector extends WireData {
*
* @param string $key
* @param mixed $value
* @return $this
* @return Selector|WireData
*
*/
public function set($key, $value) {
@@ -160,6 +294,8 @@ abstract class Selector extends WireData {
* Return the operator used by this Selector
*
* Strict standards don't let us make static abstract methods, so this one throws an exception if it's not reimplemented.
*
* #pw-internal
*
* @return string
* @throws WireException
@@ -196,7 +332,7 @@ abstract class Selector extends WireData {
$matches = false;
$values1 = is_array($this->value) ? $this->value : array($this->value);
$field = $this->field;
$operator = $this->getOperator();
$operator = $this->operator();
// prepare the value we are comparing
if(is_object($value)) {
@@ -284,13 +420,15 @@ abstract class Selector extends WireData {
$str = ($this->not ? '!' : '') .
(is_null($this->group) ? '' : $this->group . '@') .
(is_array($this->field) ? implode('|', $this->field) : $this->field) .
$this->getOperator() .
$this->operator() .
(is_array($this->value) ? implode("|", $this->value) : $openingQuote . $this->value . $closingQuote);
return $str;
}
/**
* Add all individual selector types to the runtime Selectors
*
* #pw-internal
*
*/
static public function loadSelectorTypes() {

View File

@@ -1,43 +1,49 @@
<?php namespace ProcessWire;
require_once(PROCESSWIRE_CORE_PATH . "Selector.php");
/**
* ProcessWire Selectors
*
* Processes a Selector string and can then be iterated to retrieve each resulting Selector object.
* #pw-summary Processes a selector string into a WireArray of Selector objects.
* #pw-summary-static-helpers Static helper methods useful in analyzing selector strings outside of this class.
* #pw-body =
* This Selectors class is used internally by ProcessWire to provide selector string (and array) matching throughout the core.
*
* ~~~~~
* $selectors = new Selectors("sale_price|retail_price>100, currency=USD|EUR");
* if($selectors->matches($page)) {
* // selector string matches the given $page (which can be any Wire-derived item)
* }
* ~~~~~
* ~~~~~
* // iterate and display what's in this Selectors object
* foreach($selectors as $selector) {
* echo "<p>";
* echo "Field(s): " . implode('|', $selector->fields) . "<br>";
* echo "Operator: " . $selector->operator . "<br>";
* echo "Value(s): " . implode('|', $selector->values) . "<br>";
* echo "</p>";
* }
* ~~~~~
* #pw-body
*
* @link https://processwire.com/api/selectors/ Official Selectors Documentation
* @method Selector[] getIterator()
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* https://processwire.com
*
*/
require_once(PROCESSWIRE_CORE_PATH . "Selector.php");
class Selectors extends WireArray {
/**
* Maximum length for a selector value
*
*/
const maxValueLength = 500;
/**
* Maximum length for a selector operator
*
*/
const maxOperatorLength = 10;
/**
* Maximum length for a selector field name
*
*/
const maxFieldLength = 50;
/**
* Maximum number of selectors that can be present in a given selectors string
*
*/
const maxSelectors = 20;
/**
* Static array of Selector types of $operator => $className
*
@@ -104,6 +110,11 @@ class Selectors extends WireArray {
/**
* Set the selector string or array (if not set already from the constructor)
*
* ~~~~~
* $selectors = new Selectors();
* $selectors->init("sale_price|retail_price>100, currency=USD|EUR");
* ~~~~~
*
* @param string|array $selector
*
*/
@@ -120,6 +131,8 @@ class Selectors extends WireArray {
/**
* Set the selector string
*
* #pw-internal
*
* @param string $selectorStr
*
*/
@@ -131,6 +144,8 @@ class Selectors extends WireArray {
/**
* Import items into this WireArray.
*
* #pw-internal
*
* @throws WireException
* @param string|WireArray $items Items to import.
* @return WireArray This instance.
@@ -148,6 +163,8 @@ class Selectors extends WireArray {
/**
* Per WireArray interface, return true if the item is a Selector instance
*
* #pw-internal
*
* @param Selector $item
* @return bool
*
@@ -158,6 +175,8 @@ class Selectors extends WireArray {
/**
* Per WireArray interface, return a blank Selector
*
* #pw-internal
*
*/
public function makeBlankItem() {
@@ -169,6 +188,8 @@ class Selectors extends WireArray {
*
* Static since there may be multiple instances of this Selectors class at runtime.
* See Selector.php
*
* #pw-internal
*
* @param string $operator
* @param string $class
@@ -184,6 +205,10 @@ class Selectors extends WireArray {
/**
* Return array of all valid operator characters
*
* #pw-group-static-helpers
*
* @return array
*
*/
static public function getOperatorChars() {
@@ -192,6 +217,8 @@ class Selectors extends WireArray {
/**
* Does the given string have an operator in it?
*
* #pw-group-static-helpers
*
* @param string $str
* @return bool
@@ -247,50 +274,93 @@ class Selectors extends WireArray {
}
/**
* Does the given string start with a selector?
* Is the give string a Selector string?
*
* Meaning string starts with [field][operator] like "field="
* #pw-group-static-helpers
*
* @param string $str
* @param string $str String to check for selector(s)
* @return bool
*
*/
static public function stringHasSelector($str) {
if(!self::stringHasOperator($str)) return false;
$has = false;
if(!self::stringHasOperator($str)) {
// default: has=false
} else if(preg_match('/^!?([-._a-zA-Z0-9|]+)([' . implode('', self::getOperatorChars()) . ']+)/', $str, $matches)) {
$field = $matches[1];
$operator = $matches[2];
if(in_array($field[0], array('-', '.', '|'))) {
// fields can't start with a dash or a period or a pipe
$has = false;
} else if(!isset(self::$selectorTypes[$operator])) {
// if it's not an operator we recognize then abort
$has = false;
} else {
// if we made it here, then we've found a selector
$has = true;
$alphabet = 'abcdefghijklmnopqrstuvwxyz';
// replace characters that are allowed but aren't useful here
if(strpos($str, '=(') !== false) $str = str_replace('=(', '=1,', $str);
$str = str_replace(array('!', '(', ')', '@', '.', '|', '_'), '', trim(strtolower($str)));
// flatten sub-selectors
$pos = strpos($str, '[');
if($pos && strrpos($str, ']') > $pos) {
$str = str_replace(array(']', '=[', '<[', '>['), array('', '=1,', '<2,', '>3,'), $str);
}
$str = rtrim($str, ", ");
// first character must match alphabet
if(strpos($alphabet, substr($str, 0, 1)) === false) return false;
$operatorChars = implode('', self::getOperatorChars());
if(strpos($str, ',')) {
// split the string into all key=value components and check each individually
$inQuote = '';
$cLast = '';
// replace comments in quoted values so that they aren't considered selector boundaries
for($n = 0; $n < strlen($str); $n++) {
$c = $str[$n];
if($c === ',') {
// commas in quoted values are replaced with semicolons
if($inQuote) $str[$n] = ';';
} else if(($c === '"' || $c === "'") && $cLast != "\\") {
if($inQuote && $inQuote === $c) {
$inQuote = ''; // end quote
} else if(!$inQuote) {
$inQuote = $c; // start quote
}
}
$cLast = $c;
}
$parts = explode(',', $str);
} else {
// outside of verbose mode, only the first apparent selector is checked
$parts = array($str);
}
// check each key=value component
foreach($parts as $part) {
$has = preg_match('/^[a-z][a-z0-9]*([' . $operatorChars . ']+)(.*)$/', trim($part), $matches);
if($has) {
$operator = $matches[1];
$value = $matches[2];
if(!isset(self::$selectorTypes[$operator])) {
$has = false;
} else if(self::stringHasOperator($value) && $value[0] != '"' && $value[0] != "'") {
// operators not allowed in values unless quoted
$has = false;
}
}
if(!$has) break;
}
return $has;
}
/**
* Create a new Selector object from a field name, operator, and value
*
* This is mostly for internal use, as the Selectors object already does this when you pass it
* a selector string in the constructor or init() method.
*
* #pw-group-advanced
*
* @param string $field
* @param string $operator
* @param string $value
* @return Selector
* @param string $field Field name or names (separated by a pipe)
* @param string $operator Operator, i.e. "="
* @param string $value Value or values (separated by a pipe)
* @return Selector Returns the correct type of `Selector` object that corresponds to the given `$operator`.
* @throws WireException
*
*/
@@ -303,6 +373,7 @@ class Selectors extends WireArray {
$operator = $op;
$not = true;
} else {
if(is_array($value)) $value = implode('|', $value);
$debug = $this->wire('config')->debug ? "field='$field', value='$value', selector: '$this->selectorStr'" : "";
throw new WireException("Unknown Selector operator: '$operator' -- was your selector value properly escaped? $debug");
}
@@ -315,16 +386,13 @@ class Selectors extends WireArray {
/**
* Given a selector string, return an array of (field, value, operator) for each selector in the strong.
* Given a selector string, populate to Selector objects in this Selectors instance
*
* @param string $str The string containing a selector (or multiple selectors, separated by commas)
* @return array
*
*/
protected function extractString($str) {
$cnt = 0;
while(strlen($str)) {
$not = false;
@@ -335,7 +403,7 @@ class Selectors extends WireArray {
}
$group = $this->extractGroup($str);
$field = $this->extractField($str);
$operator = $this->extractOperator($str, $this->getOperatorChars());
$operator = $this->extractOperator($str, self::getOperatorChars());
$value = $this->extractValue($str, $quote);
if($this->parseVars && $quote == '[' && $this->valueHasVar($value)) {
@@ -347,15 +415,13 @@ class Selectors extends WireArray {
}
}
if($field || strlen("$value")) {
if($field || $value || strlen("$value")) {
$selector = $this->create($field, $operator, $value);
if(!is_null($group)) $selector->group = $group;
if($quote) $selector->quote = $quote;
if($not) $selector->not = true;
$this->add($selector);
}
if(++$cnt > self::maxSelectors) break;
}
}
@@ -444,12 +510,24 @@ class Selectors extends WireArray {
protected function extractValueQuick(&$str, $openingQuote, $closingQuote) {
// determine where value ends
$commaPos = strpos("$str,", $closingQuote . ','); // "$str," just in case value is last and no trailing comma
$offset = 0;
if($openingQuote) $offset++; // skip over leading quote
$commaPos = strpos("$str,", $closingQuote . ',', $offset); // "$str," just in case value is last and no trailing comma
if($commaPos === false && $closingQuote) {
// if closing quote and comma didn't match, try to match just comma in case of "something"<space>,
$commaPos = strpos(substr($str, 1), ',');
if($commaPos !== false) $commaPos++;
$str1 = substr($str, 1);
$commaPos = strpos($str1, ',');
if($commaPos !== false) {
$closingQuotePos = strpos($str1, $closingQuote);
if($closingQuotePos > $commaPos) {
// comma is in quotes and thus not one we want to work with
return false;
} else {
// increment by 1 since it was derived from a string at position 1 (rather than 0)
$commaPos++;
}
}
}
if($commaPos === false) {
@@ -592,7 +670,7 @@ class Selectors extends WireArray {
$len = strlen("$value");
if($len) {
$str = substr($str, $n);
if($len > self::maxValueLength) $value = substr($value, 0, self::maxValueLength);
// if($len > self::maxValueLength) $value = substr($value, 0, self::maxValueLength);
}
$str = ltrim($str, ' ,"\']})'); // should be executed even if blank value
@@ -613,6 +691,8 @@ class Selectors extends WireArray {
/**
* Given a value string with an "api_var" or "api_var.property" return the string value of the property
*
* #pw-internal
*
* @param string $value var or var.property
* @return null|string Returns null if it doesn't resolve to anything or a string of the value it resolves to
@@ -634,6 +714,8 @@ class Selectors extends WireArray {
* Set whether or not vars should be parsed
*
* By default this is true, so only need to call this method to disable variable parsing.
*
* #pw-internal
*
* @param bool $parseVars
*
@@ -645,6 +727,8 @@ class Selectors extends WireArray {
/**
* Does the given Selector value contain a parseable value?
*
* #pw-internal
*
* @param Selector $selector
* @return bool
*
@@ -665,6 +749,8 @@ class Selectors extends WireArray {
* Does the given value contain an API var reference?
*
* It is assumed the value was quoted in "[value]", and the quotes are not there now.
*
* #pw-internal
*
* @param string $value The value to evaluate
* @return bool
@@ -683,6 +769,47 @@ class Selectors extends WireArray {
return true;
}
/**
* Return array of all field names referenced in all of the Selector objects here
*
* @param bool $subfields Default is to allow "field.subfield" fields, or specify false to convert them to just "field".
* @return array Returned array has both keys and values as field names (same)
*
*/
public function getAllFields($subfields = true) {
$fields = array();
foreach($this as $selector) {
$field = $selector->field;
if(!is_array($field)) $field = array($field);
foreach($field as $f) {
if(!$subfields && strpos($f, '.')) {
list($f, $subfield) = explode('.', $f, 2);
if($subfield) {} // ignore
}
$fields[$f] = $f;
}
}
return $fields;
}
/**
* Return array of all values referenced in all Selector objects here
*
* @return array Returned array has both keys and values as field values (same)
*
*/
public function getAllValues() {
$values = array();
foreach($this as $selector) {
$value = $selector->value;
if(!is_array($value)) $value = array($value);
foreach($value as $v) {
$values[$v] = $v;
}
}
return $values;
}
/**
* Does the given Wire match these Selectors?
*
@@ -757,6 +884,8 @@ class Selectors extends WireArray {
/**
* Create this Selectors object from an array
*
* #pw-internal
*
* @param array $a
* @throws WireException
@@ -883,6 +1012,7 @@ class Selectors extends WireArray {
if(isset($data['value'])) throw new WireException("You may not specify both 'value' and 'find' at the same time");
// if(!is_array($data['find'])) throw new WireException("Selector 'find' property must be specified as array");
$find = $data['find'];
$data['value'] = array();
}
if(isset($data['whitelist']) && $data['whitelist'] !== null) {
@@ -967,6 +1097,14 @@ class Selectors extends WireArray {
$fields[] = $_name;
}
// convert WireArray types to an array of $_values
if(count($_values) === 1) {
$value = reset($_values);
if(is_object($value) && $value instanceof WireArray) {
$_values = explode('|', (string) $value);
}
}
// determine value(s)
foreach($_values as $value) {
$_sanitize = $sanitize;
@@ -1017,6 +1155,8 @@ class Selectors extends WireArray {
* - If you need a literal comma, use a double comma ",,".
* - If you need a literal equals, use a double equals "==".
*
* #pw-group-static-helpers
*
* @param string $s
* @return array
*
@@ -1052,6 +1192,8 @@ class Selectors extends WireArray {
/**
* Given an assoc array, convert to a key=value selector-style string
*
* #pw-group-static-helpers
*
* @param $a
* @return string
*
@@ -1066,6 +1208,47 @@ class Selectors extends WireArray {
return rtrim($s, ", ");
}
/**
* Get the first selector that uses given field name
*
* This is useful for quickly retrieving values of reserved properties like "include", "limit", "start", etc.
*
* Using **$or:** By default this excludes selectors that have fields in an OR expression, like "a|b|c".
* So if you specified field "a" it would not be matched. If you wanted it to still match, specify true
* for the $or argument.
*
* Using **$all:** By default only the first matching selector is returned. If you want it to return all
* matching selectors in an array, then specify true for the $all argument. This changes the return value
* to always be an array of Selector objects, or a blank array if no match.
*
* @param string $fieldName Name of field to return value for (i.e. "include", "limit", etc.)
* @param bool $or Allow fields that appear in OR expressions? (default=false)
* @param bool $all Return an array of all matching Selector objects? (default=false)
* @return Selector|array|null Returns null if field not present in selectors (or blank array if $all mode)
*
*/
public function getSelectorByField($fieldName, $or = false, $all = false) {
$selector = null;
$matches = array();
foreach($this as $sel) {
if($or) {
if(!in_array($fieldName, $sel->fields)) continue;
} else {
if($sel->field() !== $fieldName) continue;
}
if($all) {
$matches[] = $sel;
} else {
$selector = $sel;
break;
}
}
return $all ? $matches : $selector;
}
/**
* See if the given $selector specifies the given $field somewhere
*

View File

@@ -17,18 +17,18 @@
*
* @see https://processwire.com/api/ref/session/ Session documentation
*
* @method User login() login($name, $pass) Login the user identified by $name and authenticated by $pass. Returns the user object on successful login or null on failure.
* @method User login() login($name, $pass, $force = false) Login the user identified by $name and authenticated by $pass. Returns the user object on successful login or null on failure.
* @method Session logout() logout() Logout the current user, and clear all session variables.
* @method void redirect() redirect($url, $http301 = true) Redirect this session to the specified URL.
* @method void init() Initialize session (called automatically by constructor) #pw-hooker
* @method bool authenticate(User $user, $pass) #pw-hooker
* @method bool isValidSession($userID) #pw-hooker
* @method bool allowLogin($name) #pw-hooker
* @method bool allowLogin($name, User $user = null) #pw-hooker
* @method void loginSuccess(User $user) #pw-hooker
* @method void loginFailure($name, $reason) #pw-hooker
* @method void logoutSuccess(User $user) #pw-hooker
*
* @property SessionCSRF $CSRF
* @property SessionCSRF $CSRF
*
* Expected $config variables include:
* ===================================
@@ -249,6 +249,10 @@ class Session extends Wire implements \IteratorAggregate {
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_httponly', 1);
ini_set('session.gc_maxlifetime', $this->config->sessionExpireSeconds);
if($this->config->sessionCookieDomain) {
ini_set('session.cookie_domain', $this->config->sessionCookieDomain);
}
if(ini_get('session.save_handler') == 'files') {
if(ini_get('session.gc_probability') == 0) {
@@ -339,27 +343,67 @@ class Session extends Wire implements \IteratorAggregate {
/**
* Generate a session fingerprint
*
* If the `$mode` argument is omitted, the mode is pulled from `$config->sessionFingerprint`. If using the
* mode argument, specify one of the following:
*
* - 0 or false: Fingerprint nothing.
* - 1 or true: Fingerprint on with default/recommended setting (currently 10).
* - 2: Fingerprint only the remote IP.
* - 4: Fingerprint only the forwarded/client IP (can be spoofed).
* - 8: Fingerprint only the useragent.
* - 10: Fingerprint the remote IP and useragent (default).
* - 12: Fingerprint the forwarded/client IP and useragent.
* - 14: Fingerprint the remote IP, forwarded/client IP and useragent (all).
*
* If using fingerprint in an environment where the users IP address may change during the session, you should
* fingerprint only the useragent, or disable fingerprinting.
*
* If using fingerprint with an AWS load balancer, you should use one of the options that uses the “client IP”
* rather than the “remote IP”, fingerprint only the useragent, or disable fingerprinting.
*
* #pw-internal
*
* @param int|bool|null $mode Optionally specify fingerprint mode (default=$config->sessionFingerprint)
* @param bool $debug Return non-hashed fingerprint for debugging purposes? (default=false)
* @return bool|string Returns false if fingerprints not enabled. Returns string if enabled.
*
*/
protected function getFingerprint() {
public function getFingerprint($mode = null, $debug = false) {
$debugInfo = array();
$useFingerprint = $mode === null ? $this->config->sessionFingerprint : $mode;
$useFingerprint = $this->config->sessionFingerprint;
if(!$useFingerprint) return false;
if(is_bool($useFingerprint) || $useFingerprint == 1) {
// default (boolean true)
$useFingerprint = self::fingerprintRemoteAddr | self::fingerprintUseragent;
if($debug) $debugInfo[] = 'default';
}
$fingerprint = '';
if($useFingerprint & self::fingerprintRemoteAddr) $fingerprint .= $this->getIP(true);
if($useFingerprint & self::fingerprintClientAddr) $fingerprint .= $this->getIP(false, 2);
if($useFingerprint & self::fingerprintRemoteAddr) {
$fingerprint .= $this->getIP(true);
if($debug) $debugInfo[] = 'remote-addr';
}
if($useFingerprint & self::fingerprintClientAddr) {
$fingerprint .= $this->getIP(false, 2);
if($debug) $debugInfo[] = 'client-addr';
}
if($useFingerprint & self::fingerprintUseragent) {
$fingerprint .= isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
if($debug) $debugInfo[] = 'useragent';
}
if($debug) {
$fingerprint = implode(',', $debugInfo) . ': ' . $fingerprint;
} else {
$fingerprint = md5($fingerprint);
}
$fingerprint = md5($fingerprint);
return $fingerprint;
}
@@ -395,9 +439,7 @@ class Session extends Wire implements \IteratorAggregate {
*/
public function get($key, $_key = null) {
if($key == 'CSRF') {
if(!$this->sessionInit) $this->init(); // init required for CSRF
if(is_null($this->CSRF)) $this->CSRF = $this->wire(new SessionCSRF());
return $this->CSRF;
return $this->CSRF();
} else if(!is_null($_key)) {
// namespace
return $this->getFor($key, $_key);
@@ -704,27 +746,42 @@ class Session extends Wire implements \IteratorAggregate {
*
*/
public function ___login($name, $pass, $force = false) {
/** @var User|null $user */
$user = null;
/** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
/** @var Users $users */
$users = $this->wire('users');
/** @var int $guestUserID */
$guestUserID = $this->wire('config')->guestUserPageID;
$fail = true;
$failReason = '';
$user = null;
if(is_object($name) && $name instanceof User) {
$user = $name;
$name = $user->name;
} else {
$name = $this->wire('sanitizer')->pageNameUTF8($name);
$name = $sanitizer->pageNameUTF8($name);
}
if(!$this->allowLogin($name)) {
$this->loginFailure($name, "User is not allowed to login");
return null;
}
if(!strlen($name)) return null;
if(is_null($user)) {
$user = strlen($name) ? $this->wire('users')->get("name=$name") : null;
$user = $users->get('name=' . $sanitizer->selectorValue($name));
}
if( $user && $user->id
&& $user->id != $this->wire('config')->guestUserPageID
&& ($force === true || $this->authenticate($user, $pass))) {
if(!$user || !$user->id) {
$failReason = 'Unknown user';
} else if($user->id == $guestUserID) {
$failReason = 'Guest user may not login';
} else if(!$this->allowLogin($name, $user)) {
$failReason = 'Login not allowed';
} else if($force === true || $this->authenticate($user, $pass)) {
$this->trackChange('login', $this->wire('user'), $user);
session_regenerate_id(true);
@@ -738,7 +795,8 @@ class Session extends Wire implements \IteratorAggregate {
$this->set('_user', 'challenge', $challenge);
$secure = $this->config->sessionCookieSecure ? (bool) $this->config->https : false;
// set challenge cookie to last 30 days (should be longer than any session would feasibly last)
setcookie(session_name() . '_challenge', $challenge, time()+60*60*24*30, '/', null, $secure, true); // PR #1264
setcookie(session_name() . '_challenge', $challenge, time()+60*60*24*30, '/',
$this->config->sessionCookieDomain, $secure, true);
}
if($this->config->sessionFingerprint) {
@@ -749,21 +807,19 @@ class Session extends Wire implements \IteratorAggregate {
$this->wire('user', $user);
$this->get('CSRF')->resetAll();
$this->loginSuccess($user);
$fail = false;
return $user;
} else {
if(!$user || !$user->id) {
$reason = "Unknown user: $name";
} else if($user->id == $this->wire('config')->guestUserPageID) {
$reason = "Guest user may not login";
} else {
$reason = "Invalid password";
}
$this->loginFailure($name, $reason);
// authentication failed
$failReason = 'Invalid password';
}
if($fail) {
$this->loginFailure($name, $failReason);
$user = null;
}
return null;
return $user;
}
/**
@@ -815,12 +871,34 @@ class Session extends Wire implements \IteratorAggregate {
* #pw-hooker
*
* @param string $name User login name
* @param User|null $user User object
* @return bool True if allowed to login, false if not (hooks may modify this)
*
*/
public function ___allowLogin($name) {
public function ___allowLogin($name, $user = null) {
$allow = true;
if(!strlen($name)) return false;
if(!$user || !$user instanceof User) {
$name = $this->wire('sanitizer')->pageNameUTF8($name);
$user = $this->wire('users')->get("name=" . $this->wire('sanitizer')->selectorValue($name));
if(!$user || !$user->id) return false;
}
$xroles = $this->wire('config')->loginDisabledRoles;
if(!is_array($xroles) && !empty($xroles)) $xroles = array($xroles);
if($name) {}
return true;
if($user) {
if($user->isUnpublished()) {
$allow = false;
} else if(is_array($xroles)) {
foreach($xroles as $xrole) {
if($user->hasRole($xrole)) {
$allow = false;
break;
}
}
}
}
return $allow;
}
/**
@@ -892,10 +970,10 @@ class Session extends Wire implements \IteratorAggregate {
$time = time() - 42000;
$secure = $this->config->sessionCookieSecure ? (bool) $this->config->https : false;
if(isset($_COOKIE[$sessionName])) {
setcookie($sessionName, '', $time, '/', null, $secure, true);
setcookie($sessionName, '', $time, '/', $this->config->sessionCookieDomain, $secure, true);
}
if(isset($_COOKIE[$sessionName . "_challenge"])) {
setcookie($sessionName . "_challenge", '', $time, '/', null, $secure, true);
setcookie($sessionName . "_challenge", '', $time, '/', $this->config->sessionCookieDomain, $secure, true);
}
}
@@ -1133,4 +1211,32 @@ class Session extends Wire implements \IteratorAggregate {
}
}
/**
* Return an instance of ProcessWires CSRF object, which provides an API for cross site request forgery protection.
*
* ~~~~
* // output somewhere in <form> markup when rendering a form
* echo $session->CSRF->renderInput();
* ~~~~
* ~~~~
* // when processing form (POST request), check to see if token is present
* if($session->CSRF->hasValidToken()) {
* // form submission is valid
* // okay to process
* } else {
* // form submission is NOT valid
* throw new WireException('CSRF check failed!');
* }
* ~~~~
*
* @return SessionCSRF
* @see SessionCSRF::renderInput(), SessionCSRF::validate(), SessionCSRF::hasValidToken()
*
*/
public function CSRF() {
if(!$this->sessionInit) $this->init(); // init required for CSRF
if(is_null($this->CSRF)) $this->CSRF = $this->wire(new SessionCSRF());
return $this->CSRF;
}
}

View File

@@ -2,26 +2,52 @@
/**
* ProcessWire CSRF Protection
*
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* https://processwire.com
*
*/
/**
* Triggered when CSRF detected
* Exception triggered by SessionCSRF::validate() when CSRF detected
*
*/
class WireCSRFException extends WireException {}
/**
* ProcessWire CSRF Protection class
* ProcessWire CSRF Protection
*
* #pw-summary Provides an API for cross site request forgery protection.
* #pw-body =
* ~~~~
* // output somewhere in form markup when rendering a form
* echo $session->CSRF->renderInput();
* ~~~~
* ~~~~
* // when processing form (POST request), check to see if token is present
* if($session->CSRF->hasValidToken()) {
* // form submission is valid
* // okay to process
* } else {
* // form submission is NOT valid
* throw new WireException('CSRF check failed!');
* }
* ~~~~
* ~~~~
* // this alternative to hasValidToken() throws WireCSRFException when invalid
* $session->CSRF->validate();
* ~~~~
*
* #pw-body
*
*
*/
class SessionCSRF extends Wire {
/**
* Get a CSRF Token name, or create one if it doesn't yet exist
*
* #pw-group-initiating
*
* @param int|string|null $id Optional unique ID for this token
* @return string
@@ -38,6 +64,8 @@ class SessionCSRF extends Wire {
/**
* Get a CSRF Token value as stored in the session, or create one if it doesn't yet exist
*
* #pw-group-initiating
*
* @param int|string|null $id Optional unique ID for this token
* @return string
@@ -57,6 +85,8 @@ class SessionCSRF extends Wire {
/**
* Get a CSRF Token timestamp
*
* #pw-group-initiating
*
* @param int|string|null $id Optional unique ID for this token
* @return string
@@ -70,6 +100,8 @@ class SessionCSRF extends Wire {
/**
* Get a CSRF Token name and value
*
* #pw-group-initiating
*
* @param int|string|null $id Optional unique ID for this token
* @return array ("name" => "token name", "value" => "token value", "time" => created timestamp)
@@ -87,7 +119,9 @@ class SessionCSRF extends Wire {
* Get a CSRF Token name and value that can only be used once
*
* Note that a single call to hasValidToken($id) or validate($id) will invalidate the single use token.
* So call them once and store your result if you need the result multiple times.
* So call them once and store your result if you need the result multiple times.
*
* #pw-group-initiating
*
* @param int|string $id Optional unique ID/name for this token (of omitted one is generated automatically)
* @return array ("id' => "token ID", "name" => "token name", "value" => "token value", "time" => created timestamp)
@@ -111,6 +145,8 @@ class SessionCSRF extends Wire {
/**
* Returns true if the current POST request contains a valid CSRF token, false if not
*
* #pw-group-validating
*
* @param int|string|null $id Optional unique ID for this token, but required if checking a single use token.
* @return bool
@@ -139,6 +175,8 @@ class SessionCSRF extends Wire {
/**
* Throws an exception if the token is invalid
*
* #pw-group-validating
*
* @param int|string|null $id Optional unique ID for this token
* @throws WireCSRFException if token not valid
@@ -154,6 +192,8 @@ class SessionCSRF extends Wire {
/**
* Clear out token value
*
* #pw-group-resetting
*
* @param int|string|null $id Optional unique ID for this token
*
@@ -166,6 +206,8 @@ class SessionCSRF extends Wire {
/**
* Clear out all saved token values
*
* #pw-group-resetting
*
*/
public function resetAll() {
@@ -174,6 +216,15 @@ class SessionCSRF extends Wire {
/**
* Render a form input[hidden] containing the token name and value, as looked for by hasValidToken()
*
* ~~~~~
* <form method='post'>
* <input type='submit'>
* <?php echo $session->CSRF->renderInput(); ?>
* </form>
* ~~~~~
*
* #pw-group-initiating
*
* @param int|string|null $id Optional unique ID for this token
* @return string

View File

@@ -291,8 +291,11 @@ class Template extends WireData implements Saveable, Exportable {
/**
* Get the role pages that are part of this template
*
* This method returns a blank PageArray if roles haven't yet been loaded into the template.
* If the roles have previously been loaded as an array, then this method converts that array to a PageArray and returns it.
* - This method returns a blank PageArray if roles havent yet been loaded into the template.
* - If the roles have previously been loaded as an array, then this method converts that array
* to a PageArray and returns it.
* - If you make changes to returned roles, make sure to set it back to the template again with setRoles().
* Its preferable to make changes with addRole() and removeRole() methods instead.
*
* #pw-group-access
*
@@ -305,14 +308,18 @@ class Template extends WireData implements Saveable, Exportable {
if(strpos($type, 'page-') === 0) $type = str_replace('page-', '', $type);
if($type != 'view') {
$roles = $this->wire('pages')->newPageArray();
if($type !== 'view') {
$roleIDs = null;
if($type == 'edit') $roleIDs = $this->editRoles;
else if($type == 'create') $roleIDs = $this->createRoles;
else if($type == 'add') $roleIDs = $this->addRoles;
else throw new WireException("Unknown roles type: $type");
if(empty($roleIDs)) return $roles;
if($type === 'edit') {
$roleIDs = $this->editRoles;
} else if($type === 'create') {
$roleIDs = $this->createRoles;
} else if($type === 'add') {
$roleIDs = $this->addRoles;
} else {
throw new WireException("Unknown roles type: $type");
}
if(empty($roleIDs)) return $this->wire('pages')->newPageArray();
return $this->wire('pages')->getById($roleIDs);
}
@@ -362,7 +369,7 @@ class Template extends WireData implements Saveable, Exportable {
* - `edit`
* - `create`
* - `add`
* - Or a `Permission` object
* - Or a `Permission` object of `page-view` or `page-edit`
* @return bool True if template has the role, false if not
*
*/
@@ -402,37 +409,137 @@ class Template extends WireData implements Saveable, Exportable {
* #pw-group-manipulation
*
* @param array|PageArray $value Role objects or array or Role IDs.
* @param string Specify one of the following:
* @param string $type Specify one of the following:
* - `view` (default)
* - `edit`
* - `create`
* - `add`
* - Or a `Permission` object
* - Or a `Permission` object of `page-view` or `page-edit`
*
*/
public function setRoles($value, $type = 'view') {
if(strpos($type, 'page-') === 0) $type = str_replace('page-', '', $type);
if($type == 'view') {
if($type === 'view' || $type === 'page-view') {
if(is_array($value) || $value instanceof PageArray) {
$this->_roles = $value;
}
} else if(WireArray::iterable($value)) {
$roleIDs = array();
foreach($value as $v) {
if(is_int($v)) $id = $v;
else if(is_string($v) && ctype_digit($v)) $id = (int) $v;
else if($v instanceof Page) $id = $v->id;
else continue;
if(is_int($v)) {
$id = $v;
} else if(is_string($v) && ctype_digit($v)) {
$id = (int) $v;
} else if($v instanceof Page) {
$id = $v->id;
} else {
continue;
}
$roleIDs[] = $id;
}
if($type == 'edit') $this->set('editRoles', $roleIDs);
else if($type == 'create') $this->set('createRoles', $roleIDs);
else if($type == 'add') $this->set('addRoles', $roleIDs);
if($type == 'edit' || $type == 'page-edit') {
$this->set('editRoles', $roleIDs);
} else if($type == 'create' || $type == 'page-create') {
$this->set('createRoles', $roleIDs);
} else if($type == 'add' || $type == 'page-add') {
$this->set('addRoles', $roleIDs);
} else {
// @todo Some other $type, delegate to permissionByRole
}
}
}
/**
* Add a Role to this template for view, edit, create, or add permission
*
* @param Role|int|string $role Role instance, id or name
* @param string $type Type of role being added, one of: view, edit, create, add. (default=view)
* @return $this
* @throws WireException If given $role cannot be resolved
*
*/
public function addRole($role, $type = 'view') {
if(is_int($role) || is_string($role)) $role = $this->wire('roles')->get($role);
if(!$role instanceof Role) throw new WireException("addRole requires Role instance, name or id");
$roles = $this->getRoles($type);
if(!$roles->has($role)) {
$roles->add($role);
$this->setRoles($roles, $type);
}
return $this;
}
/**
* Remove a Role to this template for view, edit, create, or add permission
*
* @param Role|int|string $role Role instance, id or name
* @param string $type Type of role being added, one of: view, edit, create, add. (default=view)
* You may also specify “all” to remove the role entirely from all possible usages in the template.
* @return $this
* @throws WireException If given $role cannot be resolved
*
*/
public function removeRole($role, $type = 'view') {
if(is_int($role) || is_string($role)) {
$role = $this->wire('roles')->get($role);
}
if(!$role instanceof Role) {
throw new WireException("removeRole requires Role instance, name or id");
}
if($type == 'all') {
$types = array('create', 'add', 'edit', 'view');
$rolesPermissions = $this->rolesPermissions;
if(isset($rolesPermissions["$role->id"])) {
unset($rolesPermissions["$role->id"]);
$this->rolesPermissions = $rolesPermissions;
}
} else {
$types = array($type);
}
foreach($types as $t) {
$roles = $this->getRoles($t);
if($roles->has($role)) {
$roles->remove($role);
$this->setRoles($roles, $t);
}
}
return $this;
}
/**
* Add a permission that applies to users having a specific role with pages using this template
*
* Note that the change is not committed until you save() the template.
*
* @param Permission|int|string $permission Permission object, name, or id
* @param Role|int|string $role Role object, name or id
* @param bool $test Specify true to only test if an update would be made, without changing anything
* @return bool Returns true if an update was made (or would be made), false if not
*
*/
public function addPermissionByRole($permission, $role, $test = false) {
return $this->wire('templates')->setTemplatePermissionByRole($this, $permission, $role, false, $test);
}
/**
* Revoke a permission that applies to users having a specific role with pages using this template
*
* Note that the change is not committed until you save() the template.
*
* @param Permission|int|string $permission Permission object, name, or id
* @param Role|int|string $role Role object, name or id
* @param bool $test Specify true to only test if an update would be made, without changing anything
* @return bool Returns true if an update was made (or would be made), false if not
*
*/
public function revokePermissionByRole($permission, $role, $test = false) {
return $this->wire('templates')->setTemplatePermissionByRole($this, $permission, $role, true, $test);
}
/**
* Does this template have the given Field?
*
@@ -459,6 +566,8 @@ class Template extends WireData implements Saveable, Exportable {
*/
public function set($key, $value) {
if($key == 'cacheTime') $key = 'cache_time'; // alias
if($key == 'flags') {
$this->setFlags($value);
@@ -473,7 +582,7 @@ class Template extends WireData implements Saveable, Exportable {
if($fieldgroup) $this->setFieldgroup($fieldgroup);
else $this->error("Unable to load fieldgroup '$value' for template $this->name");
return $this;
} else if($key == 'cache_time' || $key == 'cacheTime') {
} else if($key == 'cache_time') {
$value = (int) $value;
} else {
$value = '';
@@ -529,7 +638,7 @@ class Template extends WireData implements Saveable, Exportable {
if(!is_array($value)) $value = array();
$_value = array();
foreach($value as $roleID => $permissionIDs) {
// if any one of these happend to be a role name or permission name, convert to IDs
// if any one of these happened to be a role name or permission name, convert to IDs
if(!ctype_digit("$roleID")) $roleID = $this->wire('roles')->get("name=$roleID")->id;
if(!$roleID) continue;
foreach($permissionIDs as $permissionID) {
@@ -746,7 +855,7 @@ class Template extends WireData implements Saveable, Exportable {
*
* #pw-group-manipulation
*
* @return $this|bool Returns Template if successful, or false if not
* @return Template|bool Returns Template if successful, or false if not
*
*/
public function save() {
@@ -807,7 +916,7 @@ class Template extends WireData implements Saveable, Exportable {
*
*/
public function hookFinished(HookEvent $e) {
foreach($this->wire('templates') as $template) {
foreach($e->wire('templates') as $template) {
if($template->isChanged('modified') || $template->isChanged('ns')) $template->save();
}
}
@@ -1039,7 +1148,7 @@ class Template extends WireData implements Saveable, Exportable {
*
* #pw-group-identification
*
* @param $icon Font-awesome icon name
* @param string $icon Font-awesome icon name
* @return $this
*
*/

View File

@@ -21,6 +21,14 @@ class TemplateFile extends WireData {
*/
protected $filename;
/**
* The current filename being rendered (whether prepend, main, append, etc.)
*
* @var string
*
*/
protected $currentFilename;
/**
* Optional filenames that are prepended to the render
*
@@ -85,6 +93,14 @@ class TemplateFile extends WireData {
*/
protected $profiler = null;
/**
* Stack of files that are currently being rendered
*
* @var array
*
*/
static protected $renderStack = array();
/**
* DEPRECATED: Variables that will be applied globally to this and all other TemplateFile instances
*
@@ -255,26 +271,32 @@ class TemplateFile extends WireData {
foreach($this->prependFilename as $_filename) {
if($this->halt) break;
if($this->profiler) $this->start($_filename);
$this->setCurrentFilename($_filename);
/** @noinspection PhpIncludeInspection */
require($_filename);
if($this->profiler) $this->stop();
$this->setCurrentFilename('');
}
if($this->profiler) $this->start($this->filename);
if($this->halt) {
$returnValue = 0;
} else {
$this->setCurrentFilename($this->filename);
/** @noinspection PhpIncludeInspection */
$returnValue = require($this->filename);
$this->setCurrentFilename('');
}
if($this->profiler) $this->stop();
foreach($this->appendFilename as $_filename) {
if($this->halt) break;
if($this->profiler) $this->start($_filename);
$this->setCurrentFilename($_filename);
/** @noinspection PhpIncludeInspection */
require($_filename);
if($this->profiler) $this->stop();
$this->setCurrentFilename('');
}
$out = "\n" . ob_get_contents() . "\n";
@@ -333,6 +355,58 @@ class TemplateFile extends WireData {
public function setThrowExceptions($throwExceptions) {
$this->throwExceptions = $throwExceptions ? true : false;
}
/**
* Set the current filename being rendered
*
* @param $filename
*
*/
protected function setCurrentFilename($filename) {
$this->currentFilename = $filename;
if(strlen($filename)) {
self::pushRenderStack($filename);
} else {
self::popRenderStack();
}
}
/**
* Push a filename onto the render stack
*
* #pw-internal
*
* @param string $filename
*
*/
public static function pushRenderStack($filename) {
self::$renderStack[] = $filename;
}
/**
* Pop last file off of render stack
*
* #pw-internal
*
* @return string|null item that was removed, or null if none found
*
*/
public static function popRenderStack() {
$result = array_pop(self::$renderStack);
return $result;
}
/**
* Get the current render stack
*
* This contains the files currently being rendered from first to last
*
* @return array
*
*/
public static function getRenderStack() {
return self::$renderStack;
}
/**
* The string value of a TemplateFile is it's PHP template filename OR it's class name if no filename is set
@@ -361,7 +435,97 @@ class TemplateFile extends WireData {
$this->halt = $halt ? true : false;
return $this;
}
/**
* Hookable version of translation functions, $this->_(), $this->_x(), $this->_n()
*
* #pw-hooker
*
* @param string $text
* @param array $options Additional options:
* - `file` (string): Filename where text appears
* - `context` (string): Context string
* - `textPlural` (string): Plural version of text, if provided
* - `count` (int): Quantity for plural, if plural provided
* @return string
*
protected function ___translate($text, array $options = array()) {
if(!empty($options['count']) && !empty($options['textPlural'])) {
$textdomain = $this->currentFilename ? $this->currentFilename : $this;
return _n($text, $options['textPlural'], $options['count'], $textdomain);
}
return $text;
}
*/
/**
* Translate the given text string into the current language if available.
*
* If not available, or if the current language is the native language, then it returns the text as is.
*
* #pw-group-translation
*
* @param string $text Text string to translate
* @return string
*
public function _($text) {
if($this->wire('hooks')->isHooked('TemplateFile::translate()')) {
// delegate to hooked translate() method if there are hooks
$options = array('file' => $this->currentFilename);
$_text = $this->translate($text, $options);
if($_text != $text) return $_text;
}
// map to textdomain of file being rendered
if($this->currentFilename) return __($text, $this->currentFilename);
return parent::_($text);
}
*/
/**
* Perform a language translation in a specific context
*
* Used when to text strings might be the same in English, but different in other languages.
*
* #pw-group-translation
*
* @param string $text Text for translation.
* @param string $context Name of context
* @return string Translated text or original text if translation not available.
*
public function _x($text, $context) {
if($this->wire('hooks')->isHooked('TemplateFile::translate()')) {
// delegate to hooked translate() method if there are hooks
$options = array('file' => $this->currentFilename, 'context' => $context);
$_text = $this->translate($text, $options);
if($_text != $text) return $_text;
}
// map to textdomain of file being rendered
if($this->currentFilename) return _x($text, $context, $this->currentFilename);
return parent::_x($text, $context);
}
*/
/**
* Perform a language translation with singular and plural versions
*
* #pw-group-translation
*
* @param string $textSingular Singular version of text (when there is 1 item).
* @param string $textPlural Plural version of text (when there are multiple items or 0 items).
* @param int $count Quantity used to determine whether singular or plural.
* @return string Translated text or original text if translation not available.
*
public function _n($textSingular, $textPlural, $count) {
if($this->wire('hooks')->isHooked('TemplateFile::translate()')) {
// delegate to hooked translate() method if there are hooks
$options = array('file' => $this->currentFilename, 'textPlural' => $textPlural, 'count' => $count);
$_text = $this->translate($textSingular, $options);
if($_text != $textSingular && $_text != $textPlural) return $_text;
}
// map to textdomain of file being rendered
if($this->currentFilename) return _n($textSingular, $textPlural, $count, $this->currentFilename);
return parent::_n($textSingular, $textPlural, $count);
}
*/
}

View File

@@ -13,7 +13,9 @@
* @method TemplatesArray find($selector) Return the templates matching the the given selector query. #pw-internal
* @method bool save(Template $template) Save the given Template.
* @method bool delete() delete(Template $template) Delete the given Template. Note that this will throw a fatal error if the template is in use by any pages.
* @method bool|Saveable|Template clone(Saveable $item, $name = '')
* @method bool|Saveable|Template clone(Saveable $item, $name = '') #pw-internal
* @method array getExportData(Template $template) Export Template data for external use. #pw-advanced
* @method array setImportData(Template $template, array $data) Given an array of Template export data, import it to the given Template. #pw-advanced
*
*/
class Templates extends WireSaveableItems {
@@ -107,7 +109,7 @@ class Templates extends WireSaveableItems {
* Given a template ID or name, return the matching template or NULL if not found.
*
* @param string|int $key Template name or ID
* @return Template|null
* @return Template|null|string
*
*/
public function get($key) {
@@ -178,8 +180,10 @@ class Templates extends WireSaveableItems {
$access = $this->wire(new PagesAccess());
$access->updateTemplate($item);
}
$this->wire('cache')->maintenance($item);
/** @var WireCache $cache */
$cache = $this->wire('cache');
$cache->maintenance($item);
return $result;
}
@@ -198,7 +202,9 @@ class Templates extends WireSaveableItems {
if($cnt > 0) throw new WireException("Can't delete template '{$item->name}' because it is used by $cnt pages.");
$return = parent::___delete($item);
$this->wire('cache')->maintenance($item);
/** @var WireCache $cache */
$cache = $this->wire('cache');
$cache->maintenance($item);
return $return;
}
@@ -268,6 +274,9 @@ class Templates extends WireSaveableItems {
/**
* Overridden from WireSaveableItems to retain specific keys
*
* @param array $value
* @return string
*
*/
protected function encodeData(array $value) {
@@ -367,7 +376,6 @@ class Templates extends WireSaveableItems {
*
* @param Template $template Template you want to import to
* @param array $data Import data array (must have been exported from getExportData() method).
* @return bool True if successful, false if not
* @return array Returns array with list of changes (see example in method description)
*
*/
@@ -547,7 +555,206 @@ class Templates extends WireSaveableItems {
public function getParentPages(Template $template, $checkAccess = false) {
return $this->getParentPage($template, $checkAccess, true);
}
/**
* Set a Permission for a Template for and specific Role
*
* Note: you must also save() the template to commit the change.
*
* #pw-internal
*
* @param Template $template
* @param Permission|string|int $permission
* @param Role|string|int $role
* @param bool $revoke Specify true to revoke the permission, or omit to add the permission
* @param bool $test When true, no changes are made but return value still applicable
* @return bool True if an update was made (or would be made), false if not
* @throws WireException If given unknown Role or Permission
*
*/
public function setTemplatePermissionByRole(Template $template, $permission, $role, $revoke = false, $test = false) {
if(!$template->useRoles) throw new WireException("Template $template does not have access control enabled");
$defaultPermissions = array('page-view', 'page-edit', 'page-create', 'page-add');
$updated = false;
if(is_string($role) || is_int($role)) $role = $this->wire('roles')->get($role);
if(!$role instanceof Role) throw new WireException("Unknown role for Template::setPermissionByRole");
if(is_string($permission) && in_array($permission, $defaultPermissions)) {
$permissionName = $permission;
} else if($permission instanceof Permission) {
$permissionName = $permission->name;
} else {
$permission = $this->wire('permissions')->get($permission);
$permissionName = $permission ? $permission->name : '';
}
if(in_array($permissionName, $defaultPermissions)) {
// use pre-defined view/edit/create/add roles
$roles = $template->getRoles($permissionName);
$has = $roles->has($role);
if($revoke) {
if($has) {
if($test) return true;
$roles->remove($role);
$template->setRoles($roles, $permissionName);
$updated = true;
}
} else if(!$has) {
if($test) return true;
$roles->add($role);
$template->setRoles($roles, $permissionName);
$updated = true;
}
} else if($permission instanceof Permission) {
$rolesPermissions = $template->get('rolesPermissions');
if(!is_array($rolesPermissions)) $rolesPermissions = array();
$rolePermissions = isset($rolesPermissions["$role->id"]) ? $rolesPermissions["$role->id"] : array();
$_rolePermissions = $rolePermissions;
if($revoke) {
$key = array_search("$permission->id", $rolePermissions);
if($key !== false) unset($rolePermissions[$key]);
if(!in_array("-$permission->id", $rolePermissions)) $rolePermissions[] = "-$permission->id";
} else {
$key = array_search("-$permission->id", $rolePermissions);
if($key !== false) unset($rolePermissions[$key]);
if(!in_array("$permission->id", $rolePermissions)) $rolePermissions[] = "$permission->id";
}
if($rolePermissions !== $_rolePermissions) {
if($test) return true;
$rolesPermissions["$role->id"] = $rolePermissions;
$template->set('rolesPermissions', $rolesPermissions);
$updated = true;
}
} else {
throw new WireException("Unknown permission for Templates::setPermissionByRole");
}
return $updated;
}
/**
* FUTURE USE: Is the parent/child relationship allowed?
*
* By default this method returns an associative array containing the following:
*
* - `allowed` (bool): Is the relationship allowed?
* - `reasons` (array): Array of strings containing reasons why relationship is or is not allowed.
*
* If you specify the `false` for the `verbose` option then this method just returns a boolean.
*
* @param Template|Page $parent Parent Template or Page to test.
* @param Template|Page $child Child Template or Page to test.
* @param array $options Options to modify default behavior:
* - `verbose` (bool): Return verbose array. When false, returns boolean rather than array (default=true).
* - `strict` (bool): Disallow relationships that do not match rules, even if relationship already exists (default=false).
* Note that this option only applies if method is given Page objects rather than Template objects.
* @return array|bool Returns associative array by default, or bool if the verbose option is false.
* @throws WireException if given invalid argument
*
public function allowRelationship($parent, $child, array $options = array()) {
$defaults = array(
'verbose' => true,
'strict' => false,
);
$options = array_merge($defaults, $options);
$parentPage = null;
$childPage = null;
if($child instanceof Template) {
$childTemplate = $child;
} else if($child instanceof Page) {
$childPage = $child;
$childTemplate = $child->template;
} else {
throw new WireException('Invalid argument for child');
}
if($parent instanceof Template) {
$parentTemplate = $parent;
} else if($parent instanceof Page) {
$parentPage = $parent;
$parentTemplate = $parent->template;
} else {
throw new WireException('Invalid argument for parent');
}
$reasonsNo = array();
$reasonsYes = array();
$isAlreadyParent = $parentPage && $childPage && $childPage->parent_id == $parentPage->id;
$isAlreadyParentNote = "parent/child allowed because relationship already exists";
if($isAlreadyParent) {
if($options['strict']) {
// in strict mode, existing relationships are ignored and we stick only to the rules
$isAlreadyParent = false;
} else {
$reasonsYes[] = "Given child page ($childPage) already has this parent ($parentPage)";
}
}
if($parentTemplate->noChildren) {
$reason = "Parent template “$parentTemplate” specifies “no children”";
if($isAlreadyParent) {
$reasonsYes[] = "$reason - $isAlreadyParentNote";
} else {
$reasonsNo[] = $reason;
}
}
if($childTemplate->noMove) {
$reason = "Child template “$childTemplate” specifies “no move”";
if($isAlreadyParent) {
$reasonsYes[] = "$reason - $isAlreadyParentNote";
} else {
$reasonsNo[] = $reason;
}
}
if($childTemplate->noParents > 0) {
$reason = "Child template “$childTemplate” specifies “no parents” option";
if($isAlreadyParent) {
$reasonsYes[] = "$reason - $isAlreadyParentNote";
} else {
$reasonsNo[] = $reason;
}
}
if(count($parentTemplate->childTemplates)) {
if(in_array($childTemplate->id, $parentTemplate->childTemplates)) {
$reasonsYes[] = "Parent template “$parentTemplate” specifically allows children of “$childTemplate”";
} else {
$reasonsNo[] = "Parent template “$parentTemplate” does not allow children using template “$childTemplate”";
}
}
if(count($childTemplate->parentTemplates)) {
if(in_array($parentTemplate->id, $childTemplate->parentTemplates)) {
$reasonsYes[] = "Child template “$childTemplate” specifically allows parents using template “$parentTemplate”";
} else {
$reasonsNo[] = "Child template “$childTemplate” does not allow parents using template “$parentTemplate”";
}
}
$allowed = count($reasonsNo) ? false : true;
if($options['verbose']) {
return array(
'allowed' => $allowed,
'reasons' => $allowed ? $reasonsYes : $reasonsNo,
);
}
return $allowed;
}
*/
}

View File

@@ -435,17 +435,19 @@ class User extends Page {
}
/**
* Returns the URL where this user can be edited
* Return the URL necessary to edit this user
*
* In this case we adjust the default page editor URL to ensure users are edited
* only from the Access section.
*
*
* #pw-internal
*
* @return string
*
*
* @param array|bool $options Specify boolean true to force URL to include scheme and hostname, or use $options array:
* - `http` (bool): True to force scheme and hostname in URL (default=auto detect).
* @return string URL for editing this user
*
*/
public function editUrl() {
public function editUrl($options = array()) {
return str_replace('/page/edit/', '/access/users/edit/', parent::editUrl());
}

View File

@@ -139,5 +139,25 @@ class Users extends PagesType {
'pageClass' => 'User'
));
}
/**
* Hook called just before a user is saved
*
* #pw-hooker
*
* @param Page $page The user about to be saved
* @return array Optional extra data to add to pages save query.
*
*/
public function ___saveReady(Page $page) {
/** @var User $user */
$user = $page;
if(!$user->id && $user instanceof User) {
// add guest role if user doesn't already have it
$role = $this->wire('roles')->get($this->wire('config')->guestUserRolePageID);
if($role->id && !$user->hasRole($role)) $user->addRole($role);
}
return array();
}
}

View File

@@ -15,8 +15,11 @@
* classes that are hooking into others.
* #pw-body
* #pw-order-groups common,identification,hooks,notices,changes,hooker,api-helpers
* #pw-summary-api-helpers Shortcuts to ProcessWire API variables. Access without any arguments returns the API variable. Some support arguments as shortcuts to methods in the API variable.
* #pw-summary-changes Methods to support tracking and retrieval of changes made to the object.
* #pw-summary-hooks Methods for managing hooks for an object instance or class.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2017 by Ryan Cramer
* https://processwire.com
*
* #pw-use-constants
@@ -50,7 +53,7 @@
* @property WireMailTools $mail #pw-internal
* @property WireFileTools $files #pw-internal
*
* @method changed(string $what) See Wire::___changed()
* @method changed(string $what, $old = null, $new = null) See Wire::___changed()
* @method log($str = '', array $options = array()) See Wire::___log()
* @method callUnknown($method, $arguments) See Wire::___callUnknown()
* @method Wire trackException(\Exception $e, $severe = true, $text = null)
@@ -76,7 +79,7 @@
* @method WireFileTools files() Access the $files API variable as a function. #pw-group-api-helpers
* @method WireCache|string|array|PageArray|null cache($name = '', $expire = null, $func = null) Access the $cache API variable as a function. #pw-group-api-helpers
* @method Languages|Language|NullPage|null languages($name = '') Access the $languages API variable as a function. #pw-group-api-helpers
* @method WireInput|WireInputData array|string|int|null input($type = '', $key = '', $sanitizer = '') Access the $input API variable as a function. #pw-group-api-helpers
* @method WireInput|WireInputData|array|string|int|null input($type = '', $key = '', $sanitizer = '') Access the $input API variable as a function. #pw-group-api-helpers
* @method WireInputData|string|int|array|null inputGet($key = '', $sanitizer = '') Access the $input->get() API variable as a function. #pw-group-api-helpers
* @method WireInputData|string|int|array|null inputPost($key = '', $sanitizer = '') Access the $input->post() API variable as a function. #pw-group-api-helpers
* @method WireInputData|string|int|array|null inputCookie($key = '', $sanitizer = '') Access the $input->cookie() API variable as a function. #pw-group-api-helpers
@@ -364,14 +367,54 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
*
* #pw-internal
*
* @param $method
* @param $arguments
* @param string $method
* @param array $arguments
* @return mixed
* @internal
*
*/
public function _callMethod($method, $arguments) {
return call_user_func_array(array($this, $method), $arguments);
$qty = $arguments ? count($arguments) : 0;
$result = null;
switch($qty) {
case 0:
$result = $this->$method();
break;
case 1:
$result = $this->$method($arguments[0]);
break;
case 2:
$result = $this->$method($arguments[0], $arguments[1]);
break;
case 3:
$result = $this->$method($arguments[0], $arguments[1], $arguments[2]);
break;
default:
$result = call_user_func_array(array($this, $method), $arguments);
}
return $result;
}
/**
* Call a hook method (optimization when it's known for certain the method exists)
*
* #pw-internal
*
* @param string $method Method name, without leading "___"
* @param array $arguments
* @return mixed
*
*/
public function _callHookMethod($method, array $arguments = array()) {
if(method_exists($this, $method)) {
return $this->_callMethod($method, $arguments);
}
$hooks = $this->wire('hooks');
if($hooks->isMethodHooked($this, $method)) {
$result = $hooks->runHooks($this, $method, $arguments);
return $result['return'];
} else {
return $this->_callMethod("___$method", $arguments);
}
}
/**
@@ -486,7 +529,7 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
*
* @param string $method Method or property to run hooks for.
* @param array $arguments Arguments passed to the method and hook.
* @param string $type May be either 'method' or 'property', depending on the type of call. Default is 'method'.
* @param string|array $type May be either 'method', 'property' or array of hooks (from getHooks) to run. Default is 'method'.
* @return array Returns an array with the following information:
* [return] => The value returned from the hook or NULL if no value returned or hook didn't exist.
* [numHooksRun] => The number of hooks that were actually run.
@@ -554,8 +597,8 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
* - Also considers the class parents for hooks.
*
* ~~~~~
* if($pages->hasHook('find')) {
* // the Pages::find method is hooked
* if($pages->hasHook('find()')) {
* // the Pages::find() method is hooked
* }
* ~~~~~
*
@@ -716,6 +759,10 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
* This enables you to add a new accessible property to an existing object, which will execute
* your hook implementation method when called upon.
*
* Note that adding a hook with this just makes it possible to call the hook as a property.
* Any hook property you add can also be called as a method, i.e. `$obj->foo` and `$obj->foo()`
* are the same.
*
* ~~~~~
* // Adding a hook property
* $wire->addHookProperty('Page::lastModifiedStr', function($event) {
@@ -953,7 +1000,13 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
}
if(is_null($old) || is_null($new) || $lastValue !== $new) {
$this->changed($what, $old, $new); // triggers ___changed hook
/** @var WireHooks $hooks */
$hooks = $this->wire('hooks');
if(($hooks && $hooks->isHooked('changed()')) || !$hooks) {
$this->changed($what, $old, $new); // triggers ___changed hook
} else {
$this->___changed($what, $old, $new);
}
}
if($this->trackChanges & self::trackChangesValues) {
@@ -1075,14 +1128,27 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
*
* #pw-group-changes
*
* @param bool $getValues Specify true to return an associative array containing an array of previous values, indexed by
* property name, oldest to newest. Requires Wire::trackChangesValues mode to be enabled.
* @param bool $getValues Specify one of the following, or omit for default setting.
* - `false` (bool): return array of changed property names (default setting).
* - `true` (bool): return an associative array containing an array of previous values, indexed by
* property name, oldest to newest. Requires Wire::trackChangesValues mode to be enabled.
* - `2` (int): Return array where both keys and values are changed property names.
* @return array
*
*/
public function getChanges($getValues = false) {
if($getValues) return $this->changes;
return array_keys($this->changes);
if($getValues === 2) {
$changes = array();
foreach($this->changes as $name => $value) {
if($value) {} // value ignored
$changes[$name] = $name;
}
return $changes;
} else if($getValues) {
return $this->changes;
} else {
return array_keys($this->changes);
}
}
@@ -1490,8 +1556,6 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
/**
* ProcessWire instance
*
* This will replace static fuel in PW 3.0
*
* @var ProcessWire|null
*
*/
@@ -1505,7 +1569,6 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
* #pw-internal
*
* @param ProcessWire $wire
* @return $this
*
*/
public function setWire(ProcessWire $wire) {

View File

@@ -134,7 +134,7 @@ abstract class WireAction extends WireData implements Module {
*
*/
public function ___getConfigInputfields() {
$info = $this->wire('modules')->getModuleInfoVerbose($this->className());
$info = $this->wire('modules')->getModuleInfoVerbose($this->className(), array('noCache' => true));
$fieldset = $this->wire('modules')->get('InputfieldFieldset');
$fieldset->label = $info['title'];
$fieldset->description = $info['summary'];

View File

@@ -16,6 +16,11 @@
* https://processwire.com
*
* @method WireArray and($item)
* @property int $count Number of items
* @property Wire|null $first First item
* @property Wire|null $last Last item
* @property array $keys All keys used in this WireArray
* @property array $values All values used in this WireArray
*
* #pw-order-groups traversal,retrieval,manipulation,info,output-rendering,other-data-storage,changes,fun-tools,hooker
* #pw-summary WireArray is the base iterable array type used throughout the ProcessWire framework.
@@ -104,6 +109,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
*/
public function isValidKey($key) {
// unused $key intentional for descending class/template purposes
if($key) {}
return true;
}
@@ -489,13 +495,19 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
* Returns the value of the item at the given index, or null if not set.
*
* You may also specify a selector, in which case this method will return the same result as
* the `WireArray::findOne()` method.
* the `WireArray::findOne()` method. See the $key argument description for more details on
* what can be provided.
*
* #pw-group-retrieval
*
* @param int|string|array $key Key of item to retrieve. If not specified, 0 is assumed (for first item).
* You may also provide an array of keys, in which case an array of matching items will be returned, indexed by your keys.
* @return WireData|Page|mixed|null Value of item requested, or null if it doesn't exist.
* @param int|string|array $key Provide any of the following:
* - Key of item to retrieve.
* - Array of keys, in which case an array of matching items will be returned, indexed by your keys.
* - A selector string or selector array, to return the first item that matches the selector.
* - A string of text with "{var}" tags in it that will be populated with any matching properties from this WireArray.
* - A string like "foobar[]" which returns an array of all "foobar" properties from each item in the WireArray.
* - A string containing the "name" property of any item, and the matching item will be returned.
* @return WireData|Page|mixed|array|null Value of item requested, or null if it doesn't exist.
* @throws WireException
*
*/
@@ -530,7 +542,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
if(isset($this->data[$key])) return $this->data[$key];
// check if key contains a selector
if(Selectors::stringHasOperator($key)) {
if(Selectors::stringHasSelector($key)) {
$item = $this->findOne($key);
if($item === false) $item = null;
return $item;
@@ -1399,22 +1411,32 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
$sort = array();
$start = 0;
$limit = null;
$eq = null;
// leave sort, limit and start away from filtering selectors
foreach($selectors as $selector) {
$remove = true;
$field = $selector->field;
if($selector->field === 'sort') {
if($field === 'sort') {
// use all sort selectors
$sort[] = $selector->value;
} else if($selector->field === 'start') {
} else if($field === 'start') {
// use only the last start selector
$start = (int) $selector->value;
} else if($selector->field === 'limit') {
} else if($field === 'limit') {
// use only the last limit selector
$limit = (int) $selector->value;
$limit = (int) $selector->value;
} else if(($field === 'index' || $field == 'eq') && !$this->wire('fields')->get($field)) {
// eq or index properties
switch($selector->value) {
case 'first': $eq = 0; break;
case 'last': $eq = -1; break;
default: $eq = (int) $selector->value;
}
} else {
// everything else is to be saved for filtering
@@ -1440,6 +1462,38 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
}
}
if(!is_null($eq)) {
if($eq === -1) {
$limit = -1;
$start = null;
} else if($eq === 0) {
$start = 0;
$limit = 1;
} else {
$start = $eq;
$limit = 1;
}
}
if($limit < 0 && $start < 0) {
// we don't support double negative, so double negative makes a positive
$start = abs($start);
$limit = abs($limit);
} else {
if($limit < 0) {
if($start) {
$start = $start - abs($limit);
$limit = abs($limit);
} else {
$start = count($this->data) - abs($limit);
$limit = count($this->data);
}
}
if($start < 0) {
$start = count($this->data) - abs($start);
}
}
// if $limit has been given, tell sort the amount of rows that will be used
if(count($sort)) $this->_sort($sort, $limit ? $start+$limit : null);
if($start || $limit) {
@@ -1736,7 +1790,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
* #pw-internal
*
* @param bool $trackChanges True to turn change tracking ON, or false to turn OFF. Default of true is assumed.
* @return $this
* @return Wire|WireArray
*
*/
public function resetTrackChanges($trackChanges = true) {
@@ -1753,6 +1807,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
*
*/
protected function trackAdd($item, $key) {
if($key) {}
if($this->trackChanges()) $this->itemsAdded[] = $item;
}
@@ -1764,6 +1819,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
*
*/
protected function trackRemove($item, $key) {
if($key) {}
if($this->trackChanges()) $this->itemsRemoved[] = $item;
}
@@ -1900,6 +1956,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
* - `append` (string): String to append to result. Ignored if result is blank.
* - Please note that if delimiter is omitted, $options becomes the second argument.
* @return string
* @see WireArray::each(), WireArray::explode()
*
*/
public function implode($delimiter, $property = '', array $options = array()) {
@@ -1966,6 +2023,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
* - `getMethod` (string): Method to call on each item to retrieve $property (default = "get")
* - `key` (string|null): Property of Wire objects to use for key of array, or omit (null) for non-associative array (default).
* @return array
* @see WireArray::each(), WireArray::implode()
*
*/
public function explode($property, array $options = array()) {
@@ -2043,20 +2101,55 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
* #pw-group-other-data-storage
* #pw-link [Introduction of data method](https://processwire.com/talk/topic/5098-new-wirearray-api-additions-on-dev/)
*
* @param string|null $key Name of data property you want to get or set. Omit to get all data properties.
* @param mixed|null $value Value of data property you want to set. Omit when getting properties.
* @return $this|mixed|null Returns one of the following, depending on specified arguments:
* - `mixed` when getting a single property: whatever you set is what you will get back.
* - `null` if the property you are trying to get does not exist in the data.
* - `$this` reference to this WireArray if you were setting a value.
* - `array` of all data if you specified no arguments.
*
* @param string|null|array|bool $key Name of data property you want to get or set, or:
* - Omit to get all data properties.
* - Specify associative array of [property => value] to set multiple properties.
* - Specify associative array and boolean TRUE for $value argument to replace all data with the new array given in $key.
* - Specify regular array of property names to return multiple properties.
* - Specify boolean FALSE to unset property name specified in $value argument.
* @param mixed|null|bool $value Value of data property you want to set. Omit when getting properties.
* - Specify boolean TRUE to replace all data with associative array of data given in $key argument.
* @return WireArray|mixed|array|null Returns one of the following, depending on specified arguments:
* - `mixed` when getting a single property: whatever you set is what you will get back.
* - `null` if the property you are trying to get does not exist in the data.
* - `$this` reference to this WireArray if you were setting a value.
* - `array` of all data if you specified no arguments or requested multiple keys.
*
*/
public function data($key = null, $value = null) {
if(is_null($key) && is_null($value)) return $this->extraData;
if(is_null($value)) return isset($this->extraData[$key]) ? $this->extraData[$key] : null;
$this->extraData[$key] = $value;
if($key === null && $value === null) {
// get all properties
return $this->extraData;
} else if(is_array($key)) {
// get or set multiple properties
if($value === true) {
// replace all data with data in given $key array
$this->extraData = $key;
} else {
// test if array is associative
if(ctype_digit(implode('0', array_keys($key)))) {
// regular, non-associative array, GET only requested properties
$a = array();
foreach($key as $k) {
$a[$k] = isset($this->extraData[$k]) ? $this->extraData[$k] : null;
}
return $a;
} else if(count($key)) {
// associative array, setting multiple values to extraData
$this->extraData = array_merge($this->extraData, $key);
}
}
} else if($key === false && is_string($value)) {
// unset a property
unset($this->extraData[$value]);
} else if($value === null) {
// get a property
return isset($this->extraData[$key]) ? $this->extraData[$key] : null;
} else {
// set a property
$this->extraData[$key] = $value;
}
return $this;
}
@@ -2091,6 +2184,8 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
// if keys are not numeric, we delegete numbers to eq(n)
return $this->eq((int) $key);
}
} else if(is_callable($key) || (is_string($key) && strpos($key, '{') !== false && strpos($key, '}'))) {
return $this->each($key);
}
return $this->get($key);
}
@@ -2202,6 +2297,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
* (if using option #3 in arguments).
* - `array` (4): Returns an array of associative arrays containing the property values for each item
* you requested (if using option #4 in arguments).
* @see WireArray::implode(), WireArray::explode()
*
*/
public function each($func = null) {
@@ -2262,12 +2358,12 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
$info['items'] = array();
foreach($this->data as $key => $value) {
if(is_object($value)) {
if(method_exists($value, 'path')) {
if($value instanceof Page) {
$value = '/' . ltrim($value->path(), '/');
} else if($value instanceof WireData) {
$_value = $value;
$value = $value->name;
if(!$value) $value = $_value->id;
$value = $value->get('name');
if(!$value) $value = $_value->get('id');
if(!$value) $value = $_value->className();
} else {
// keep $value as it is

View File

@@ -107,6 +107,8 @@ class WireCache extends Wire {
*
* After a preloaded cache is returned from a get() call, it is removed from local storage.
*
* #pw-group-advanced
*
* @param string|array $names
* @param int|string|null $expire
*
@@ -119,6 +121,8 @@ class WireCache extends Wire {
/**
* Preload all caches for the given object or namespace
*
* #pw-group-advanced
*
* @param object|string $ns
* @param int|string|null $expire
*
@@ -134,18 +138,39 @@ class WireCache extends Wire {
*
* Optionally specify expiration time and/or a cache generation function to use when cache needs to be created.
*
* Cached value can be a string, an array of non-object values, or a PageArray.
*
* ~~~~~
* // get single cache value
* $str = $cache->get('foo');
*
* // get 3 cached values, returns associative array with foo, bar, baz indexes
* $array = $cache->get([ 'foo', 'bar', 'baz' ]);
*
* // get all cache values with names starting with “hello”
* $array = $cache->get('hello*');
*
* // get cache only if its less than or equal to 1 hour old (3600 seconds)
* $str = $cache->get('foo', 3600);
*
* // same as above, but also generates the cache value with function when expired
* $str = $cache->get('foo', 3600, function() {
* return "This is the cached value";
* });
* ~~~~~
*
* @param string|array $name Provide a single cache name, an array of cache names, or an asterisk cache name.
* If given a single cache name (string) just the contents of that cache will be returned.
* If given an array of names, multiple caches will be returned, indexed by cache name.
* If given a cache name with an asterisk in it, it will return an array of all matching caches.
* - If given a single cache name (string) just the contents of that cache will be returned.
* - If given an array of names, multiple caches will be returned, indexed by cache name.
* - If given a cache name with an asterisk in it, it will return an array of all matching caches.
* @param int|string|null $expire Optionally specify max age (in seconds) OR oldest date string.
* If cache exists and is older, then blank returned. You may omit this to divert to whatever expiration
* was specified at save() time. Note: The $expire and $func arguments may optionally be reversed.
* If using a $func, the behavior of $expire becomes the same as that of save().
* - If cache exists and is older, then blank returned. You may omit this to divert to whatever expiration
* was specified at save() time. Note: The $expire and $func arguments may optionally be reversed.
* - If using a $func, the behavior of $expire becomes the same as that of save().
* @param callable $func Optionally provide a function/closure that generates the cache value and it
* will be used when needed.This option requires that only one cache is being retrieved (not an array of caches).
* will be used when needed. This option requires that only one cache is being retrieved (not an array of caches).
* Note: The $expire and $func arguments may optionally be reversed.
* @return string|array|PageArray|mixed|null Returns null if cache doesn't exist and no generation function provided.
* @return string|array|PageArray|mixed|null Returns null if cache doesnt exist and no generation function provided.
* @throws WireException if given invalid arguments
*
*
@@ -320,11 +345,25 @@ class WireCache extends Wire {
/**
* Same as get() but with namespace
*
* Namespace is useful to avoid cache name collisions. The ProcessWire core commonly uses cache
* namespace to bind cache values to the object class, which often make a good namespace.
*
* Please see the `$cache->get()` method for usage of all arguments.
*
* ~~~~~
* // specify namespace as a string
* $value = $cache->getFor('my-namespace', 'my-cache-name');
*
* // or specify namespace is an object instance
* $value = $cache->get($this, 'my-cache-name');
* ~~~~~
*
* @param string|object $ns Namespace
* @param string $name
* @param null|int|string $expire
* @param callable|null $func
* @return string|array
* @param string $name Cache name
* @param null|int|string $expire Optional expiration
* @param callable|null $func Optional cache generation function
* @return string|array|PageArray|mixed|null Returns null if cache doesnt exist and no generation function provided.
* @see WireCache::get()
*
*/
public function getFor($ns, $name, $expire = null, $func = null) {
@@ -335,17 +374,27 @@ class WireCache extends Wire {
/**
* Save data to cache with given name
*
* ~~~~~
* $value = "This is the value that will be cached.";
*
* // cache the value, using default expiration (daily)
* $cache->save("my-cache-name", $value);
*
* // cache the value, and expire after 1 hour (3600 seconds)
* $cache->save("my-cache-name", $value, 3600);
* ~~~~~
*
* @param string $name Name of cache, can be any string up to 255 chars
* @param string|array|PageArray $data Data that you want to cache
* @param int|Page $expire Lifetime of this cache, in seconds
* ...or specify: WireCache::expireHourly, WireCache::expireDaily, WireCache::expireWeekly, WireCache::expireMonthly
* ...or specify the future date you want it to expire (as unix timestamp or any strtotime compatible date format)
* ...or provide a Page object to expire when any page using that template is saved.
* ...or specify: WireCache::expireNever to prevent expiration.
* ...or specify: WireCache::expireSave to expire when any page or template is saved.
* ...or specify: Selector string matching pages that, when saved, expire the cache.
* @param string|array|PageArray $data Data that you want to cache. May be string, array of non-object values, or PageArray.
* @param int|Page $expire Lifetime of this cache, in seconds, OR one of the following:
* - Specify one of the `WireCache::expire*` constants.
* - Specify the future date you want it to expire (as unix timestamp or any `strtotime()` compatible date format)
* - Provide a `Page` object to expire when any page using that template is saved.
* - Specify `WireCache::expireNever` to prevent expiration.
* - Specify `WireCache::expireSave` to expire when any page or template is saved.
* - Specify selector string matching pages thatwhen savedexpire the cache.
* @return bool Returns true if cache was successful, false if not
* @throws WireException if given uncachable data
* @throws WireException if given data that cannot be cached
*
*/
public function save($name, $data, $expire = self::expireDaily) {
@@ -375,6 +424,8 @@ class WireCache extends Wire {
$data = json_encode($data);
if($data === false) throw new WireException("Unable to encode array data for cache: $name");
}
if(is_null($data)) $data = '';
$sql =
'INSERT INTO caches (`name`, `data`, `expires`) VALUES(:name, :data, :expires) ' .
@@ -401,11 +452,28 @@ class WireCache extends Wire {
/**
* Same as save() except with namespace
*
* @param string|object $ns Namespace
* @param $name
* @param $data
* @param int $expire
* @return bool
* Namespace is useful to avoid cache name collisions. The ProcessWire core commonly uses cache
* namespace to bind cache values to the object class, which often make a good namespace.
*
* ~~~~~
* // save cache using manually specified namespace
* $cache->save("my-namespace", "my-cache-name", $value);
*
* // save cache using namespace of current object
* $cache->save($this, "my-cache-name", $value);
* ~~~~~
*
* @param string|object $ns Namespace for cache
* @param string $name Name of cache, can be any string up to 255 chars
* @param string|array|PageArray $data Data that you want to cache
* @param int|Page $expire Lifetime of this cache, in seconds, OR one of the following:
* - Specify one of the `WireCache::expire*` constants.
* - Specify the future date you want it to expire (as unix timestamp or any strtotime compatible date format)
* - Provide a `Page` object to expire when any page using that template is saved.
* - Specify `WireCache::expireNever` to prevent expiration.
* - Specify `WireCache::expireSave` to expire when any page or template is saved.
* - Specify selector string matching pages that, when saved, expire the cache.
* @return bool Returns true if cache was successful, false if not
*
*/
public function saveFor($ns, $name, $data, $expire = self::expireDaily) {
@@ -414,7 +482,7 @@ class WireCache extends Wire {
}
/**
* Given a $expire seconds, date, page, or template convert it to an ISO-8601 date
* Given an expiration seconds, date, page, or template, convert it to an ISO-8601 date
*
* Returns an array of expires info requires multiple parts, like with self::expireSelector.
* In this case it returns array with array('expires' => date, 'selector' => selector);
@@ -490,7 +558,15 @@ class WireCache extends Wire {
}
/**
* Delete/clear the cache(s) identified by $name
* Delete/clear the cache(s) identified by given name or wildcard
*
* ~~~~~
* // Delete cache named "my-cache-name"
* $cache->delete("my-cache-name");
*
* // Delete all caches starting with "my-"
* $cache->delete("my-*");
* ~~~~~
*
* @param string $name Name of cache, or partial name with wildcard (i.e. "MyCache*") to clear multiple caches.
* @return bool True on success, false on failure
@@ -519,10 +595,18 @@ class WireCache extends Wire {
}
/**
* Delete the cache identified by $name within given namespace ($ns)
* Delete one or more caches in a given namespace
*
* ~~~~~
* // Delete all in namespace
* $cache->deleteFor("my-namespace");
*
* // Delete one cache in namespace
* $cache->deleteFor("my-namespace", "my-cache-name");
* ~~~~~
*
* @param string $ns
* @param string $name If none specified, all for $ns are deleted
* @param string $ns Namespace of cache.
* @param string $name Name of cache. If none specified, all for namespace are deleted.
* @return bool True on success, false on failure
*
*/
@@ -536,6 +620,9 @@ class WireCache extends Wire {
* Cache maintenance removes expired caches
*
* Should be called as part of a regular maintenance routine and after page/template save/deletion.
* ProcessWire already calls this automatically, so you dont typically need to call this method on your own.
*
* #pw-group-advanced
*
* @param Template|Page|null|bool Item to run maintenance for or, if not specified, general maintenance is performed.
* General maintenance only runs once per request. Specify boolean true to force general maintenance to run.
@@ -788,6 +875,8 @@ class WireCache extends Wire {
/**
* Get information about all the caches in this WireCache
*
* #pw-group-advanced
*
* @param bool $verbose Whether to be more verbose for human readability
* @param string $name Optionally specify name of cache to get info. If omitted, all caches are included.
* @return array of arrays of cache info
@@ -866,6 +955,8 @@ class WireCache extends Wire {
/**
* Save to the cache log
*
* #pw-internal
*
* @param string $str Message to log
* @param array $options

View File

@@ -23,6 +23,8 @@
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* https://processwire.com
*
* @method WireArray and($items = null)
*
*/
@@ -57,7 +59,7 @@ class WireData extends Wire implements \IteratorAggregate, \ArrayAccess {
*
*/
public function set($key, $value) {
if($key == 'data') {
if($key === 'data') {
if(!is_array($value)) $value = (array) $value;
return $this->setArray($value);
}
@@ -200,16 +202,24 @@ class WireData extends Wire implements \IteratorAggregate, \ArrayAccess {
* $value = $item->data('some_property');
* ~~~~~
*
* @param string $key Property you want to get or set.
* @param mixed $value Optionally specify a value if you want to set rather than get.
* @return mixed|null|$this Returns one of the following:
* @param string|array $key Property you want to get or set, or associative array of properties you want to set.
* @param mixed $value Optionally specify a value if you want to set rather than get.
* Or Specify boolean TRUE if setting an array via $key and you want to overwrite any existing values (rather than merge).
* @return array|WireData|null Returns one of the following:
* - `mixed` - Actual value if getting a previously set value.
* - `null` - If you are attempting to get a value that has not been set.
* - `$this` - If you are setting a value.
*/
public function data($key = null, $value = null) {
if(is_null($key)) return $this->data;
if(is_null($value)) {
if(is_array($key)) {
if($value === true) {
$this->data = $key;
} else {
$this->data = array_merge($this->data, $key);
}
return $this;
} else if(is_null($value)) {
return isset($this->data[$key]) ? $this->data[$key] : null;
} else {
$this->data[$key] = $value;

View File

@@ -1,35 +1,57 @@
<?php namespace ProcessWire;
/**
* ProcessWire Database Backup and Restore
*
* #pw-summary ProcessWire Database Backup and Restore
* #pw-summary-initialization Its not typically necessary to call these initialization methods unless doing manual initialization.
* #pw-var $backup
* #pw-instantiate $backup = $database->backup();
* #pw-order-groups actions,reporting,initialization,advanced
* #pw-body =
* This class intentionally does not have any external dependencies (other than PDO)
* so that it can be included by outside tools for restoring/exporting, with the main
* example of that being the ProcessWire installer.
*
* The recommended way to access these backup methods is via the `$database` API variable
* method `$database->backups()`, which returns a `WireDatabaseBackup` instance, however
* you can also initialize the class manually if you prefer, like this:
* ~~~~~
* // determine where backups will go (should NOT be web accessible)
* $backupPath = $config->paths->assets . 'backups/';
*
* // create a new WireDatabaseBackup instance
* $backup = new WireDatabaseBackup($backupPath);
*
* // Option 1: set the already-connected DB connection
* $backup->setDatabase($this->database);
*
* // Option 2: OR provide a Config object that contains the DB connection info
* $backup->setDatabaseConfig($this->config);
*
* ~~~~~
* ### Backup the database
* ~~~~~
* $file = $backup->backup();
* if($file) {
* echo "Backed up to: $file";
* } else {
* echo "Backup failed: " . implode("<br>", $backup->errors());
* }
* ~~~~~
*
* ### Restore a database
* ~~~~~
* $success = $backup->restore($file);
* if($success) {
* echo "Restored database from file: $file";
* } else {
* echo "Restore failed: " . implode("<br>", $backup->errors());
* }
* ~~~~~
* #pw-body
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* https://processwire.com
*
* USAGE
*
* Initialization
* ==============
* $backup = new WireDatabaseBackup('/path/to/backups/');
* $backup->setDatabase($this->database); // optional, if omitted it will attempt it's own connection
* $backup->setDatabaseConfig($this->config); // optional, only if setDatabase() was called
*
* Backup
* ======
* $file = $backup->backup([$options]);
* if($file) print_r($backup->notes());
* else print_r($backup->errors());
*
*
* Restore
* =======
* $success = $backup->restore($file, [$options]);
* if($success) print_r($backup->notes());
* else print_r($backup->errors());
*
*/
@@ -241,7 +263,9 @@ class WireDatabaseBackup {
* You should follow-up the construct call with one or both of the following:
*
* - $backups->setDatabase(PDO|WireDatabasePDO);
* - $backups->setDatabaseConfig(array|object);
* - $backups->setDatabaseConfig(array|object);
*
* #pw-group-initialization
*
* @param string $path Path where database files are stored
* @throws \Exception
@@ -254,6 +278,8 @@ class WireDatabaseBackup {
/**
* Set the current ProcessWire instance
*
* #pw-internal
*
* @param ProcessWire $wire
*
*/
@@ -264,8 +290,16 @@ class WireDatabaseBackup {
/**
* Set the database configuration information
*
* @param array|object $config Containing these properties: dbUser, dbHost, dbPort, dbName,
* and optionally: dbPass, dbPath, dbCharset
* #pw-group-initialization
*
* @param array|Config|object $config Containing these properties:
* - dbUser
* - dbHost
* - dbPort
* - dbName
* - dbPass
* - dbPath (optional)
* - dbCharset (optional)
* @return $this
* @throws \Exception if missing required config settings
*
@@ -301,7 +335,9 @@ class WireDatabaseBackup {
}
/**
* Set the database connection
* Set the PDO database connection
*
* #pw-group-initialization
*
* @param \PDO|WireDatabasePDO $database
* @throws \PDOException on invalid connection
@@ -318,6 +354,8 @@ class WireDatabaseBackup {
/**
* Get current database connection, initiating the connection if not yet active
*
* #pw-advanced
*
* @return null|\PDO|WireDatabasePDO
* @throws \Exception
*
@@ -349,6 +387,8 @@ class WireDatabaseBackup {
/**
* Add an error and return last error
*
* #pw-group-reporting
*
* @param string $str If omitted, no error is added
* @return string
*
@@ -361,6 +401,8 @@ class WireDatabaseBackup {
/**
* Return all error messages that occurred
*
* #pw-group-reporting
*
* @param bool $reset Specify true to clear out existing errors or omit just to return error messages
* @return array
*
@@ -373,6 +415,8 @@ class WireDatabaseBackup {
/**
* Record a note
*
* #pw-group-reporting
*
* @param $key
* @param $value
@@ -385,6 +429,8 @@ class WireDatabaseBackup {
/**
* Get all notes
*
* #pw-group-reporting
*
* @param bool $reset
* @return array
@@ -399,6 +445,8 @@ class WireDatabaseBackup {
/**
* Set path where database files are stored
*
* #pw-group-initialization
*
* @param string $path
* @return $this
* @throws \Exception if path has a problem
@@ -411,7 +459,15 @@ class WireDatabaseBackup {
$this->path = $path;
return $this;
}
/**
* Get path where database files are stored
*
* #pw-group-reporting
*
* @return string
*
*/
public function getPath() {
return $this->path;
}
@@ -420,6 +476,8 @@ class WireDatabaseBackup {
* Return array of all backup files
*
* To get additional info on any of them, call getFileInfo($basename) method
*
* #pw-group-reporting
*
* @return array of strings (basenames)
*
@@ -439,8 +497,10 @@ class WireDatabaseBackup {
/**
* Get information about a backup file
*
* #pw-group-reporting
*
* @param $filename
* @param string $filename
* @return array Returns associative array of information on success, empty array on failure
*
*/
@@ -506,6 +566,8 @@ class WireDatabaseBackup {
/**
* Get array of all table names
*
* #pw-group-reporting
*
* @param bool $count If true, returns array will be indexed by name and include count of records as value
* @param bool $cache Allow use of cache?
@@ -548,9 +610,28 @@ class WireDatabaseBackup {
/**
* Perform a database export/dump
*
* @param array $options See $backupOptions
* @return string Full path and filename of database export file, or false on failure.
* #pw-group-actions
*
* @param array $options Options to modify default behavior:
* - `filename` (string): filename for backup: default is to make a dated filename, but this can also be used (basename only, no path)
* - `description` (string): optional description of this backup
* - `tables` (array): if specified, export will only include these tables
* - `user` (string): username to associate with the backup file (string), optional
* - `excludeTables` (array): exclude creating or inserting into these tables
* - `excludeCreateTables` (array): exclude creating these tables, but still export data
* - `excludeExportTables` (array): exclude exporting data, but still create tables
* - `whereSQL` (array): SQL conditions for export of individual tables [table => [SQL conditions]]. The `table` portion (index) may also be a full PCRE regexp, must start with `/` to be recognized as regex.
* - `maxSeconds` (int): max number of seconds allowed for execution (default=1200)
* - `allowDrop` (bool): use DROP TABLES statements before CREATE TABLE statements? (default=true)
* - `allowUpdate` (bool): use UPDATE ON DUPLICATE KEY so that INSERT statements can UPDATE when rows already present (all tables). (default=false)
* - `allowUpdateTables` (array): table names that will use UPDATE ON DUPLICATE KEY (does NOT require allowUpdate=true)
* - `findReplace` (array): find and replace in row data during backup. Example: ['databass' => 'database']
* - `findReplaceCreateTable` (array): find and replace in create table statements
* Example: ['DEFAULT CHARSET=latin1;' => 'DEFAULT CHARSET=utf8;']
* - `extraSQL` (array): additional SQL queries to append at the bottom. Example: ['UPDATE pages SET created=NOW()']
* @return string Full path and filename of database export file, or false on failure.
* @throws \Exception on fatal error
* @see WireDatabaseBackup::restore()
*
*/
public function backup(array $options = array()) {
@@ -595,7 +676,16 @@ class WireDatabaseBackup {
return $success ? $file : false;
}
/**
* Set backup options
*
* #pw-internal
*
* @param array $options
* @return $this
*
*/
public function setBackupOptions(array $options) {
$this->backupOptions = array_merge($this->backupOptions, $options);
return $this;
@@ -824,12 +914,26 @@ class WireDatabaseBackup {
/**
* Import a database SQL file that was created by this class
* Restore/import a MySQL database dump file
*
* This method is designed to restore dump files created by the backup() method of this
* class, however it *may* also work with dump files created from other sources like
* mysqldump or PhpMyAdmin.
*
* #pw-group-actions
*
* @param string $filename Filename to restore, optionally including path (if no path, then path set to construct is assumed)
* @param array $options See WireDatabaseBackup::$restoreOptions
* @param array $options Options to modify default behavior:
* - `tables` (array): table names to restore (empty=all)
* - `allowDrop` (bool): allow DROP TABLE statements (default=true)
* - `haltOnError` (bool): halt execution when an error occurs? (default=false)
* - `maxSeconds` (int): max number of seconds allowed for execution (default=1200)
* - `findReplace` (array): find and replace in row data. Example: ['databass' => 'database']
* - `findReplaceCreateTable` (array): find and replace in create table statements.
* Example: ['DEFAULT CHARSET=utf8;' => 'DEFAULT CHARSET=utf8mb4;']
* @return true on success, false on failure. Call the errors() method to retrieve errors.
* @throws \Exception on fatal error
* @see WireDatabaseBackup::backup()
*
*/
public function restore($filename, array $options = array()) {
@@ -856,7 +960,16 @@ class WireDatabaseBackup {
return $success;
}
/**
* Set restore options
*
* #pw-internal
*
* @param array $options
* @return $this
*
*/
public function setRestoreOptions(array $options) {
$this->restoreOptions = array_merge($this->restoreOptions, $options);
return $this;
@@ -996,6 +1109,8 @@ class WireDatabaseBackup {
*
* This method assumes both files follow the SQL dump format created by this class.
*
* #pw-advanced
*
* @param string $filename1 Original filename
* @param string $filename2 Filename that may have statements that will update/override those in filename1
* @param array $options
@@ -1090,6 +1205,8 @@ class WireDatabaseBackup {
/**
* Returns array of all create table statements, indexed by table name
*
* #pw-internal
*
* @param string $filename to extract all CREATE TABLE statements from
* @param array $options
* @return bool|array of CREATE TABLE statements, associative: indexed by table name
@@ -1112,7 +1229,9 @@ class WireDatabaseBackup {
}
/**
* Returns array of all INSERT statements, indexed by table name
* Returns array of all INSERT statements in given filename, indexed by table name
*
* #pw-internal
*
* @param string $filename to extract all CREATE TABLE statements from
* @return array of arrays of INSERT statements. Base array is associative indexed by table name.

View File

@@ -63,6 +63,14 @@ class WireDatabasePDO extends Wire implements WireDatabase {
*/
protected $init = false;
/**
* Strip 4-byte characters in “quote” and “escapeStr” methods? (only when dbEngine is not utf8mb4)
*
* @var bool
*
*/
protected $stripMB4 = false;
/**
* PDO connection settings
*
@@ -107,6 +115,8 @@ class WireDatabasePDO extends Wire implements WireDatabase {
$name = $config->dbName;
$socket = $config->dbSocket;
$charset = $config->dbCharset;
$options = $config->dbOptions;
$initCommand = str_replace('{charset}', $charset, $config->dbInitCommand);
if($socket) {
@@ -118,13 +128,17 @@ class WireDatabasePDO extends Wire implements WireDatabase {
if($port) $dsn .= ";port=$port";
}
$driver_options = array(
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
);
if(!is_array($options)) $options = array();
if(!isset($options[\PDO::ATTR_ERRMODE])) {
$options[\PDO::ATTR_ERRMODE] = \PDO::ERRMODE_EXCEPTION;
}
if($initCommand) $driver_options[\PDO::MYSQL_ATTR_INIT_COMMAND] = $initCommand;
if($initCommand && !isset($options[\PDO::MYSQL_ATTR_INIT_COMMAND])) {
$options[\PDO::MYSQL_ATTR_INIT_COMMAND] = $initCommand;
}
$database = new WireDatabasePDO($dsn, $username, $password, $driver_options);
$database = new WireDatabasePDO($dsn, $username, $password, $options);
$database->setDebugMode($config->debug);
$config->wire($database);
$database->_init();
@@ -161,6 +175,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
if($this->init || !$this->isWired()) return;
$this->init = true;
$config = $this->wire('config');
$this->stripMB4 = $config->dbStripMB4 && strtolower($config->dbEngine) != 'utf8mb4';
$this->queryLogMax = (int) $config->dbQueryLogMax;
$sqlModes = $config->dbSqlModes;
if(is_array($sqlModes)) {
@@ -625,7 +640,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
*
*/
public function escapeStr($str) {
return substr($this->pdo()->quote($str), 1, -1);
return substr($this->quote($str), 1, -1);
}
/**
@@ -654,6 +669,9 @@ class WireDatabasePDO extends Wire implements WireDatabase {
*
*/
public function quote($str) {
if($this->stripMB4 && is_string($str) && !empty($str)) {
$str = $this->wire('sanitizer')->removeMB4($str);
}
return $this->pdo()->quote($str);
}
@@ -733,13 +751,13 @@ class WireDatabasePDO extends Wire implements WireDatabase {
/**
* Retrieve new instance of WireDatabaseBackups ready to use with this connection
*
* See WireDatabaseBackup class for usage.
* See `WireDatabaseBackup` class for usage.
*
* #pw-group-custom
*
* @return WireDatabaseBackup
* @throws WireException|\Exception on fatal error
* @see WireDatabaseBackup
* @see WireDatabaseBackup::backup(), WireDatabaseBackup::restore()
*
*/
public function backups() {

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