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

578 Commits

Author SHA1 Message Date
Ryan Cramer
649d2569ab Bump version to 3.0.123 2018-12-21 14:30:49 -05:00
Ryan Cramer
df4ab1dbd3 Update for processwire/processwire-issues#408 InputfieldSelector count() to wireCount() 2018-12-20 11:52:01 -05:00
Ryan Cramer
05367367c0 Add $options argument support to WireMailTools::new(), i.e. $mail->new(array $options) 2018-12-20 11:17:02 -05:00
Ryan Cramer
67898cb36a Add additional mbstring check to Sanitizer and add two new whitespace reduction methods (for internal use) 2018-12-20 11:15:34 -05:00
Ryan Cramer
e20b6917d3 Fix issue processwire/processwire-issues#724 UTF-8 pagenames in non-default language 2018-12-20 10:58:26 -05:00
Ryan Cramer
a0570bb2a0 Update for processwire/processwire-issues#767 2018-12-19 12:24:22 -05:00
Ryan Cramer
1b059b0ced Bump version to 3.0.122 plus some other minor adjustments 2018-12-14 14:24:50 -05:00
Ryan Cramer
a89543944d Update WireArray::import() to support duplicate items when duplicate checking is turned off per processwire/processwire-issues#767 2018-12-14 13:12:52 -05:00
Ryan Cramer
b773c81ae9 Fix issue processwire/processwire-issues#766 auto-remove UTF-8 value of &#8232 line-seperator entity in $sanitizer->text() function 2018-12-14 13:07:26 -05:00
Ryan Cramer
e8d2eed1ba Add @horst-n fix for processwire/processwire-issues#715 2018-12-14 11:05:07 -05:00
Ryan Cramer
05fd707b9c Fix issue processwire/processwire-issues#763 for ProcessRole, plus this commit also contains some minor tweaks to ProcessPageLister 2018-12-14 09:03:23 -05:00
Ryan Cramer
c1b9349a7c Fix issue with Safari column widths in AdminThemeUikit that use showIf conditions per processwire/processwire-issues#480 2018-12-14 07:58:26 -05:00
Ryan Cramer
6801e2df94 Fix issue processwire/processwire-issues#756 make link modal files selection not require being open before it can be populated with files from page selection 2018-12-14 06:01:02 -05:00
Ryan Cramer
6d1b558d85 Fix issue processwire/processwire-issues#723 2018-12-12 14:28:24 -05:00
Ryan Cramer
365d49bcaa Fix issue processwire/processwire-issues#749 2018-12-12 14:19:57 -05:00
Ryan Cramer
a40f24d722 A couple of updates to account for new PHP 7.2/7.3 notices per processwire/processwire-issues#408 2018-12-12 11:59:02 -05:00
Ryan Cramer
f39d4387d5 Various minor tweaks/updates and bump version to 3.0.121 2018-12-07 14:58:03 -05:00
Ryan Cramer
3b6b5eea7d Update locale warning message in ProcessLogin per processwire/processwire-issues#732 2018-12-07 12:09:57 -05:00
Ryan Cramer
3e690b8ea4 Update phpdoc @return statement for Page::index() 2018-12-07 10:15:32 -05:00
Ryan Cramer
af0afe9f95 Additional updates for processwire/processwire-issues#751 plus some enhancements to PageFinder 2018-12-07 09:57:07 -05:00
Ryan Cramer
0dc8766491 Fix issue processwire/processwire-issues#751 with Page::index() not working for hidden/unpublished pages 2018-12-06 14:05:17 -05:00
Ryan Cramer
d7b7acb8e5 Various minor adjustments and bump version to 3.0.120 2018-11-30 13:52:24 -05:00
Ryan Cramer
0442572885 Fix issue processwire/processwire-issues#758 unable to select "17:10:02" time format in FieldtypeDatetime 2018-11-30 13:42:15 -05:00
Ryan Cramer
65b3edb761 Fix issue processwire/processwire-issues#750 where WireShutdown email was missing a send() call 2018-11-30 13:21:33 -05:00
Ryan Cramer
9aa0de6351 Fix issue processwire/processwire-issues#743 2018-11-30 13:15:27 -05:00
Ryan Cramer
45e8a00395 Fix issue processwire/processwire-issues#741 2018-11-30 12:29:20 -05:00
Ryan Cramer
a8d4a6cd46 Add phpdoc for Template::icon per processwire/processwire-issues#739 2018-11-30 12:03:49 -05:00
Ryan Cramer
91fdca02ea Fix issue processwire/processwire-issues#728 2018-11-29 14:12:33 -05:00
Ryan Cramer
ffdc1031de Fix issue processwire/processwire-issues#727 2018-11-29 14:02:46 -05:00
Ryan Cramer
c7447bf838 Fix issue processwire/processwire-issues#726 where Pages > Tree > dropdown was displaying liternal icon names (in addition to actual icon) in some cases when it shouldn't 2018-11-29 13:59:36 -05:00
Ryan Cramer
009dec332c Fix issue processwire/processwire-issues#725 where fallback email for comment notifications not working if hostname had port number 2018-11-29 13:31:05 -05:00
Ryan Cramer
e6213c23b3 Attempt fix for issue processwire/processwire-issues#724 where creating page from page reference field using UTF8 charset was failing. 2018-11-29 13:19:40 -05:00
Ryan Cramer
0cb693c450 Fix issue processwire/processwire-issues#719 where 'parent' property didn't work for $page->setAndSave() 2018-11-29 12:01:57 -05:00
Ryan Cramer
fcc353bc3c Updates per processwire/processwire-issues#704 with WireTempDir log entries 2018-11-29 11:12:11 -05:00
Ryan Cramer
cc212b3254 Add hasNext() and hasPrev() methods to PaginatedArray class 2018-11-21 10:49:53 -05:00
Ryan Cramer
b083c96717 Test commit, please ignore 2018-11-20 17:01:10 -05:00
Ryan Cramer
e61bb4982a Bump version to 3.0.119 2018-11-16 16:05:14 -05:00
Ryan Cramer
fad1e11575 Add a setting() function to the Functions API for more convenient management of site-specific runtime settings 2018-11-16 11:39:12 -05:00
Ryan Cramer
3eecb6cf25 Update Pageimage::getVariations() and Pageimage::removeVariations() methods with several new options, primarily to support a new PageAction module for clearing out old image variations 2018-11-16 09:51:06 -05:00
Ryan Cramer
96f62b313f Add WireArray::slices($qty) method that returns the WireArray sliced into $qty equal parts (new WireArray objects) 2018-11-09 14:26:32 -05:00
Ryan Cramer
32f594de2a Update Pages Export/Import class to support export/import of page created and modified dates 2018-11-09 14:25:34 -05:00
Ryan Cramer
93a9747657 Add @BitPoet processwire/processwire-requests#233 to make ajax file upload use chunked method for potential reduced memory usage and support of larger file uploads 2018-11-08 09:28:03 -05:00
Ryan Cramer
c146c71de3 Add @Toutouwai processwire/processwire-requests#242 to support automatic open of collapsed Inputfield when file dragged into file/image or CKEditor field that supports it 2018-11-08 09:04:04 -05:00
Ryan Cramer
7629e518e3 Remove unintended dev constant from AdminThemeUikit.module 2018-11-05 14:58:56 -05:00
Ryan Cramer
a46336de00 Bump version to 3.0.118 2018-11-02 08:07:24 -04:00
Ryan Cramer
e65c287099 Update AdminThemeUikit Uikit version to rc17 2018-11-02 06:55:19 -04:00
Ryan Cramer
a355bd74e6 Add renderedExtras() hook to ProcessPageLister, plus make the finalSelector property modifiable if needed by hooks, per request from @renobird 2018-11-02 06:44:49 -04:00
Ryan Cramer
a0192327e0 Some refactoring in ProcessPageEdit, plus fix issue where user having page-edit-lang-[lang] permission while having non-default language selected in their profile could receive incorrect "missing required field" error for Page Name, when making edits. 2018-11-02 06:41:35 -04:00
Ryan Cramer
09ee56b41d Add new “What URLs redirect to this page?” fieldset in the page editor Settings tab (near the bottom). This section enables you to see all URLs that redirect to the page, and also lets you add new redirect URLs or remove existing ones. Currently available to superusers only. 2018-10-31 10:29:06 -04:00
Ryan Cramer
6fb6837406 Add $page->addUrl($url, [$language]); and $page->removeUrl($url); methods that allow you to add or remove redirects to a page programatically. This is provided by updates to the PagePathHistory module, which also received several unrelated updates, like support for virtual path history, which is historical URLs for a page determined by changes to parent pages. 2018-10-31 10:22:47 -04:00
Ryan Cramer
54537e77fa Add new $files->unlink() and $files->rename() methods to files API var, plus enhance many file methods with additional security for path verification, and update PW's various usages of unlink/rename to use the new versions provided by the files API var. 2018-10-31 09:30:15 -04:00
Ryan Cramer
0f9eb0aaf5 Add hasValue(), hasTitle() and hasID() methods to SelectableOptionArray class 2018-10-26 12:21:10 -04:00
Ryan Cramer
7331bac132 Addition of a $page->numParents() method/property which reflects the number of parents the page has, aka depth in the tree. This commit also has several small adjustments and fixes, including a fix for the issue introduced last week that caused issues with WireArray in versions of PHP prior to 7.x 2018-10-26 12:18:46 -04:00
Ryan Cramer
b964fd1a15 Bump version to 3.0.117 2018-10-19 14:48:19 -04:00
Ryan Cramer
d12a78d07b Update WireArray class to support usage as a general purpose array (that can hold strings, numbers, etc.), rather than being limited to support of Wire derived objects. Also add WireArray::new($items) static method that lets you create a new WireArray and add $items to it in one shot. The same new() method works on any WireArray derived type such as PageArray and any others. 2018-10-19 11:14:29 -04:00
Ryan Cramer
f5cda0acdb Update the FieldtypeComments classes to support for more output configuration options for CommentForm and CommentList 2018-10-19 11:12:58 -04:00
Ryan Cramer
faaf8adec9 Add support for a "pw-optional" or "data-pw-optional" attribute to Markup Regions. When a defined region of markup has this boolean attribute present, the wrapping region tags will be removed from the document if they will be blank (i.e. nothing was populated to them). 2018-10-19 11:06:52 -04:00
Ryan Cramer
cf322b6dca Improvements to FieldtypeTextarea::importValue() to improve accuracy of linked asset URLs on pages imported from one site to another where the root URL has changed (subdirectory vs. non subdirectory), as well as better handling asset URLs where the link included a hostname 2018-10-19 11:04:30 -04:00
Ryan Cramer
0b8382ab3e A few small improvements to the PagesExportImport class 2018-10-19 11:02:54 -04:00
Ryan Cramer
6367fb7668 Forgot to bump version to 3.0.116 on Friday, so this fixes that 2018-10-08 13:45:46 -04:00
Ryan Cramer
e2b6453a96 Fix issue processwire/processwire-issues#718 FieldtypeFloat float typecast in getPrecision() method 2018-10-05 10:00:12 -04:00
Ryan Cramer
8e22bee1b5 Fix issue processwire/processwire-issues#716 where $page traversal methods like next() and prev() were not working on RepeaterPage items 2018-10-05 09:48:20 -04:00
Ryan Cramer
5fddd95b43 Add new Page::numDescendants() method and property, plus descendants() and descendant() alias methods. Add Page::findOne() method. Update ProcessPageList with the ability to customize what is shown in the numChildren/count shown for each Page, along with the ability to display the the newly added Page descendants numbers instead of or in addition to the Page numChildren. 2018-10-05 08:11:16 -04:00
Ryan Cramer
faa0d4f4df Update ProcessPageTrash module to be more clear, showing confirmation before page list, and adding option to specify time limit. Also now reports more verbose info about the trash operation including a "pages trashed per second" rate for comparison purposes on different DB engines and settings. Related to processwire/processwire-issues#692 2018-10-03 11:50:30 -04:00
Ryan Cramer
e9c7178a22 Optimizations and updates to PagesTrash class emptyTrash() process, plus add transaction support to empty trash process per processwire/processwire-issues#692 though currently I'm not seeing any effect from it 2018-10-03 11:46:22 -04:00
Ryan Cramer
0ec70c875f Update PagesEditor::savePageStatus() method to also accept array of page IDs (rather than just 1 page ID). Also accepts Page or PageArray objects now too. 2018-10-03 11:45:07 -04:00
Ryan Cramer
17c872b079 Set a default user-agent header in WireHttp to resolve issues where accessed hostname seems to require it (noticed this on a couple hosts running Varnish, which seem to throw a 400/403 if no user-agent header present) 2018-10-03 06:10:26 -04:00
Ryan Cramer
546340652f Attempt fix for issue processwire/processwire-issues#661 where cached PageList data could re-appear when closing then re-opening an item in the PageList. Was an issue if an item was moved or trashed, because it could appear to still be in the old location in the PageList even when it wasn't. This commit also refactors several parts in ProcessPageList.js. 2018-10-01 11:21:43 -04:00
Ryan Cramer
11cb8deaa2 Bump version to 3.0.115 2018-09-28 14:23:16 -04:00
Ryan Cramer
3ab9b358e5 Update ProcessLogin to support a configuration option to specify roles that should be prompted to enable two-factor authentication 2018-09-28 11:14:19 -04:00
Ryan Cramer
2f2bb118b0 Update ProcessProfile to use a dialog window prompt (via vex) when saving changes to a field requires current password. 2018-09-28 10:31:50 -04:00
Ryan Cramer
c55a547458 Add allowOriginal option to Pageimage::size() and Pageimage::maxSize() to avoid creating variations when original image is already at target dimension 2018-09-27 08:48:02 -04:00
Ryan Cramer
45f0c14831 Adjustment for issue processwire/processwire-issues#709 2018-09-27 07:54:57 -04:00
Ryan Cramer
e6bcd3e44c Fix issue processwire/processwire-issues#712 to correct issue with $config->sessionHistory=1; setting not working correctly 2018-09-27 07:39:53 -04:00
Ryan Cramer
a1bac571f3 Fix issue processwire/processwire-issues#708 Modules refresh showing unnecessary error message 2018-09-26 11:11:54 -04:00
Ryan Cramer
42511ebb77 Additional tweak for processwire/processwire-issues#709 2018-09-26 11:04:53 -04:00
Ryan Cramer
42ec0e3cf4 Some refactoring in WireTempDir class and attempt fix of issue processwire/processwire-issues#704 2018-09-26 09:40:40 -04:00
Ryan Cramer
bf8baf36dd Add new methods to $files API var: fileInPath($file, $path); unixDirName($dir); unixFileName($file); filePutContents($file, $contents); 2018-09-26 09:40:16 -04:00
Ryan Cramer
e5900e7567 Attempt fix for processwire/processwire-issues#704 2018-09-25 10:50:54 -04:00
Ryan Cramer
bddea16255 Clarify wording in phpdoc of Wire::trackChanges() method per processwire/processwire-issues#707 2018-09-25 10:26:02 -04:00
Ryan Cramer
f2edf04602 Fix issue processwire/processwire-issues#709 where Pageimage::maxSize() could crop when it shouldn't when source image smaller than maxSize requested dimensions 2018-09-25 10:13:51 -04:00
Ryan Cramer
3b4cdeb991 Make it possible to add Pagefile from another Page to Pagefiles WireArray per request processwire/processwire-issues#710 2018-09-25 09:32:39 -04:00
Ryan Cramer
96bd37eec9 Bump version to 3.0.114 2018-09-21 14:03:32 -04:00
Ryan Cramer
b4aec46a67 Add $database->supportsTransaction($table = '') method to return true or false as to whether the database (or a specific table) supports transactions 2018-09-21 13:30:57 -04:00
Ryan Cramer
9a7e5d5bd3 Add PagefilesManager->importFiles() and PagefilesManager->replaceFiles() methods 2018-09-21 13:29:47 -04:00
Ryan Cramer
f0cc6f1134 Update to support module autoload order per request from @adrianbj 2018-09-21 12:29:15 -04:00
Ryan Cramer
8add252813 Fix issue processwire/processwire-issues#700 where ProcessPermission "add new" was incorrectly showing Save+Publish buttons 2018-09-20 11:58:20 -04:00
Ryan Cramer
8973a9de49 Bump version to 3.0.113 2018-09-14 15:29:00 -04:00
Ryan Cramer
d7a84f223c Update CKEditor version from 4.8.0 to 4.10.1 2018-09-14 15:26:48 -04:00
Ryan Cramer
42b46152eb Refactoring of SessionLoginThrottle. Prevents it from being too aggressive when TFA is in use, improves clarity of message to user, and adds the ability to log failures. 2018-09-14 12:03:16 -04:00
Ryan Cramer
64680df68f Refactoring of ProcessController and add support for access controlled methods that works with the existing moduleInfo['nav'][]['permission'] setting. Previously that permission was only used to determine whether to show the item in the nav, and you had to do your own separate permission check in the actual method implementation. This commit also moves all the WireException definitions to the main core/Exceptions file. 2018-09-14 11:02:30 -04:00
Ryan Cramer
590a69502c Update $mail (WireMailTools) API var with some fluent interface shortcuts, enabling you to start with $mail->to(mail), $mail->from(email), or $mail->subject(email), all of which return a WireMail instance, rather than having to start with a $mail->new() to get the WireMail instance. While minor, this makes for a little bit shorter API calls when using WireMail fluent interface. 2018-09-14 10:36:14 -04:00
Ryan Cramer
c1c705ab59 Refactoring of WireShutdown class and related minor updates to some other classes 2018-09-14 10:34:56 -04:00
Ryan Cramer
89caff1bdc Fix issue processwire/processwire-issues#697 pages with $template->noMove setting could not be restored/moved out of the trash 2018-09-13 12:11:43 -04:00
Ryan Cramer
0284bd28a1 Fix issue processwire/processwire-issues#695 change order of listable() call in ProcessPageList::renderNavJSON so that one could make an viewable/editable page non-listable in the tree nav dropdown 2018-09-13 11:17:21 -04:00
Ryan Cramer
84e5fd6b51 Fix issue processwire/processwire-issues#690 where isMoveable() error messages were not yet multi-language translatable 2018-09-13 10:33:40 -04:00
Ryan Cramer
2c9b25fdfd Fix issue processwire/processwire-issues#686 where FieldtypeCheckbox::markupValue() result ended up blank after a PHP strip_tags() 2018-09-13 10:07:29 -04:00
Ryan Cramer
272077b1cf Fix issue processwire/processwire-issues#685 where templates with noParents=-1 setting (only one allowed) were not shown as selectable in Template field in page editor 2018-09-13 09:08:44 -04:00
Ryan Cramer
818d9f50a6 Fix issue processwire/processwire-issues#683 where $pages->findOne() didn't have exclusion/filter enabled if finding unpublished or hidden page that is editable & viewable to current user when the selector includes only reference to the page's id. 2018-09-13 08:50:49 -04:00
Ryan Cramer
efb7a8ace2 Fix issue processwire/processwire-issues#682 fix typo about what tab to change the unpublished setting 2018-09-13 08:34:27 -04:00
Ryan Cramer
844946b706 Fix issue processwire/processwire-issues#681 where Fieldtype exceptions getting caught when saving page, which could interfere with InnoDB transactions when Fieldtype throws an Exception 2018-09-13 08:29:49 -04:00
Ryan Cramer
dfc0c4da52 Fix issue processwire/processwire-issues#680 where ProcessLanguageTranslator does not skip over /site/assets/ on Windows servers 2018-09-13 06:21:31 -04:00
Ryan Cramer
adca762ee1 Fix issue processwire/processwire-issues#679 capture Exception in ProcessLanguageTranslator when it throws due to too many directories 2018-09-13 06:12:13 -04:00
Ryan Cramer
468ecb6b29 Make ProcessPageSearchLive::execute() hookable per processwire/processwire-issues#675 2018-09-12 13:20:26 -04:00
Ryan Cramer
7a89f4f188 Fix issue processwire/processwire-issues#671 where LiveSearch pages list could also show users if custom configured field name present on user template 2018-09-12 12:57:11 -04:00
Ryan Cramer
313fee873c Fix issue processwire/processwire-issues#669 admin liveSearch noSearchTypes option not working for 'pages', 'trash' or 'modules'. 2018-09-12 12:35:49 -04:00
Ryan Cramer
8315776960 Module download/install process label updates per processwire/processwire-issues#641 2018-09-12 11:58:52 -04:00
Ryan Cramer
6c9b475559 Add @horst-n PR #118 which fixes processwire/processwire-issues#628 in ImageSizerEngine 2018-09-12 11:16:33 -04:00
Ryan Cramer
b07d7341eb Improvements and refactorings to inputfields.js, plus add processwire/processwire-requests#224, and addition of new public API methods: InputfieldFocus($inputfield) which finds, opens and focuses an Inputfield (making it visible) regardless of where it is in the interface; InputfieldOpen($inputfield) which opens a collapsed Inputfield; and InputfieldClose($inputfield) which does the opposite. Thanks to @Toutouwai for the feature request and example code that got this started. 2018-09-11 11:31:34 -04:00
Ryan Cramer
e2478aa401 Add new string case conversion methods to Sanitizer: hyphenCase(), snakeCase(), camelCase(), and pascalCase() 2018-09-10 11:15:59 -04:00
Ryan Cramer
d260b269c5 Bump version to 3.0.112 2018-09-07 14:45:21 -04:00
Ryan Cramer
77cf1b9c71 Typo fix in ProcessField 2018-09-07 14:43:15 -04:00
Ryan Cramer
af423e5406 Update WireHttp to support more HTTP response codes (the success ones, in addition to the existing error codes) 2018-09-07 14:42:04 -04:00
Ryan Cramer
7e5f79e9b3 Fix issue where drag/drop image into non-default language CKEditor field was showing error message 2018-09-04 12:01:46 -04:00
Ryan Cramer
4c48e1d941 Refactoring in ProcessPageClone.module and related hooks and multi-language support, plus some improvements to new PagesNames class. 2018-08-30 06:22:02 -04:00
Ryan Cramer
aa34e8a0f7 Bump version to 3.0.111 2018-08-24 16:41:05 -04:00
Ryan Cramer
cc53b835b6 Refactoring of the PagesTrash class and related, plus some minor additions to PagesNames class 2018-08-24 10:41:47 -04:00
Ryan Cramer
dfa8cc7b74 Refactoring of the PagesEditor class, primarily addition of a new PagesNames class for handling page names, duplicate names, untitled pages, incrementing page names, etc. 2018-08-23 10:30:12 -04:00
Ryan Cramer
088938454b Move random generation functions from Password to new WireRandom class, and add several new methods for generating random strings, numbers, arrays, etc. 2018-08-21 11:23:20 -04:00
Ryan Cramer
ff123065eb Update the clone/copy repeater item function to also account for InputfieldTable rows that may be present in the repeater 2018-08-19 07:01:54 -04:00
Ryan Cramer
5554e87b47 Fix issue processwire/processwire-issues#667 where cloning a FieldtypeOptions field was only cloning the field, and not the options. 2018-08-13 05:56:57 -04:00
Ryan Cramer
563f4d237b Bump version to 3.0.110 2018-08-10 15:32:08 -04:00
Ryan Cramer
3c923dfb93 Some updates and improvements to the Password::randomAlnum() function 2018-08-10 11:36:51 -04:00
Ryan Cramer
acd42bbfc9 Update ProcessPageView to support optional single subdir for page files when $config->pagefileSecure mode is active. This enables it to work when pagefileSecure mode is active and a module (like ProDrafts) uses a subdirectory of of a page's file/asset directory 2018-08-09 11:15:06 -04:00
Ryan Cramer
2e672428a4 Fix issue processwire/processwire-issues#659 where PageFinder "_custom" option didn't yet have support for "field.owner.property=value" selectors 2018-08-08 15:05:21 -04:00
Ryan Cramer
82641a5521 Add request processwire/processwire-issues#614 to make InputfieldPassword use version of id attribute for confirm input rather than name attribute, for cases where someone might want to have multiple password fields in the same document. 2018-08-08 14:48:10 -04:00
Ryan Cramer
9fc4ff9e21 Add request processwire/processwire-issues#657 to clarify what characters are allowed when creating templates, plus some other minor improvements in ProcessTemplate 2018-08-08 09:17:46 -04:00
Ryan Cramer
fc8f6ad51c Fix issue processwire/processwire-issues#658 where non-specified, non-default-language pageNumUrlPrefix settings weren't properly falling back to $config default setting 2018-08-08 08:05:37 -04:00
Ryan Cramer
cee8504db9 Fix issue processwire/processwire-issues#656 where recently added Inputfield::setParent() update was causing issues (ListerPro config tab as one example) 2018-08-08 06:21:01 -04:00
Ryan Cramer
6d1dc83a45 Fix issue processwire/processwire-issues#639 where template configured to unpublish when missing required field was not applying to missing required fields within repeater items. 2018-08-08 06:16:04 -04:00
Ryan Cramer
69424df158 Fix issue processwire/processwire-issues#642 where new ProcessPageSearch feature was not using correct edit URL when searching for users 2018-08-07 06:17:32 -04:00
Ryan Cramer
6cc5a01cf5 Fix issue processwire/processwire-issues#646 where repeater trash icon click not triggering InputfieldStateChanged on InputfieldRepeater on case one forgets to save their changes 2018-08-07 06:02:58 -04:00
Ryan Cramer
683e24e010 Add an update to accommodate request in processwire/processwire-issues#648 for wording of warning message when a 3rd party hook has changed a page's name behind the scenes after it was saved 2018-08-06 11:31:35 -04:00
Ryan Cramer
a1676b0adb Fix issue processwire/processwire-issues#650 cropped images in image fields with overwrite option enabled not showing 2018-08-06 10:33:17 -04:00
Ryan Cramer
6882abe40a Fix issue processwire/processwire-issues#653 Inputfield parent issue causing out of memory errors when two buttons lacking name attributes 2018-08-06 08:21:21 -04:00
Ryan Cramer
c9a210ec35 Bump version to 3.0.109 2018-08-03 12:45:52 -04:00
Ryan Cramer
f61bca5d9f A couple of other minor updates to Session and FieldtypeModule 2018-08-03 12:21:00 -04:00
Ryan Cramer
95adb8039c Updates to Tfa base class, plus updates to ProcessLogin for TFA support 2018-08-03 12:17:12 -04:00
Ryan Cramer
5af6e63358 Add two-factor authentication module base class. Two modules that implmement it coming shortly. 2018-08-02 12:33:06 -04:00
Ryan Cramer
730b61a3c3 Various minor module updates 2018-08-02 12:31:41 -04:00
Ryan Cramer
fb9d1458a8 Update FieldtypeModule to support a couple of new config options for blank value and showing a "none" option when used with radios. 2018-08-02 12:23:18 -04:00
Ryan Cramer
fa2c8e30b9 Fix PSR-4 stacked namespace loading issue in WireClassLoader 2018-08-02 12:21:58 -04:00
Ryan Cramer
75889d3ac9 Update Password class with new methods for random string generation 2018-08-02 12:19:38 -04:00
Ryan Cramer
5e389ff65a Upgrade Inputfield and InputfieldWrapper base classes to support new getting, setting, and traversal methods. Plus a new $inputfields->new() method that lets you create a new Inputfield and add it to the wrapper in one step. 2018-08-02 12:17:59 -04:00
Ryan Cramer
c01289edb7 Attempt fix for issue processwire/processwire-issues#635 with FileCompiler and PHP touch() when PW run under different user accounts 2018-07-19 06:58:21 -04:00
Ryan Cramer
22fe5794d0 Fix issue processwire/processwire-issues#634 where single-use tokens in SessionCSRF needed an automatic reset to be consistent with behavior described the phpdoc 2018-07-18 07:45:38 -04:00
Ryan Cramer
b439a4488f Fix issue processwire/processwire-issues#631 where page-edit-lang-[name] permissions could prevent intended "addable" permission on templates. 2018-07-18 06:48:59 -04:00
Ryan Cramer
b6ec6cd679 Fix issue processwire/processwire-issues#629 where $config->pagefileSecure combined with non-superuser editing user profile didn't show images in image fields 2018-07-17 11:41:33 -04:00
Ryan Cramer
411fedf785 Fix issue processwire/processwire-issues#627 InputfieldCKEditor when extraAllowedContent empty 2018-07-16 11:37:56 -04:00
Ryan Cramer
2fb5ebbc96 Fix issue processwire/processwire-issues#625 where nested repeater with image field when combined with $config->pagefileSecure and access control without guest page-view permission was not appearing for logged-in non-superuser 2018-07-16 11:31:45 -04:00
Ryan Cramer
1404d1ef0f A couple of minor adjustments for 3.0.108 2018-07-13 13:25:18 -04:00
Ryan Cramer
1f4ca1b8f4 Some additional adjustments to the live search, including the addition of a "help" option, which you can access just by typing "help" into the search box. Bumped version to 3.0.108 2018-07-13 11:19:26 -04:00
Ryan Cramer
9741fbb818 Add 'language' option to $page->editUrl() method 2018-07-12 14:54:34 -04:00
Ryan Cramer
e038cb3d89 Update 3 admin themes for live search code, plus some other minor adjustments 2018-07-12 14:53:46 -04:00
Ryan Cramer
484d183054 Add a $modules->findByInfo() method that enables finding modules by factors in their module info, without instantiating the modules. Also some optimizations to the $modules->getModuleInfo() method. 2018-07-12 10:48:22 -04:00
Ryan Cramer
f8c41366c5 Update wireInstanceOf($instance, $className)) function to support interfaces for $className argument, regardless of whether the $instance argument is an object or a string (class name). Previous versions did not fully support interfaces for the $className argument. 2018-07-12 06:22:35 -04:00
Ryan Cramer
cd7a684b85 Add "searchable" module support to ProcessPageType and ProcessUser 2018-07-11 16:13:42 -04:00
Ryan Cramer
6b2f2243da Add "searchable" module support to ProcessTemplate and ProcessField 2018-07-11 16:13:00 -04:00
Ryan Cramer
14fce7e11a Add "searchable" module support to ProcessCommentsManager 2018-07-11 16:11:34 -04:00
Ryan Cramer
be9d07c57b Add new "addable" and "deletable" options to AsmSelect, enabling you to have AsmSelects where the ability to add and/or delete items is suppressed. With both options disabled, it's useful for sorting fixed groups of items. 2018-07-11 16:09:38 -04:00
Ryan Cramer
2e095c9cf4 Add url(), httpUrl() and editUrl() methods to Comment class. 2018-07-11 16:08:08 -04:00
Ryan Cramer
0dc2f17b5a Add a couple of new static methods to Selectors class for examining selector operators 2018-07-11 16:06:52 -04:00
Ryan Cramer
04187ed19f Update PaginatedArray/PageArray::getPaginationString() method to support new options (documented in phpdoc) 2018-07-11 16:05:48 -04:00
Ryan Cramer
756c9298a5 Add SearchableModule interface to Module definition, add getProcessPage() to Process module base class (which returns the Page object the Process page lives on), plus a couple of minor phpdoc improvements in core classes 2018-07-11 16:02:40 -04:00
Ryan Cramer
c3e0bbec86 Bump version to 3.0.107 2018-06-29 15:11:41 -04:00
Ryan Cramer
30bc99cfd7 Update ProcessPageList to have module configuration setting as to whether or not Trash and Restore is available to non-superusers. Plus add new Restore tab to ProcessPageEdit that appears when editing a page in the trash. Previously you could only restore by using the PageList "restore" action. 2018-06-29 12:27:01 -04:00
Ryan Cramer
bf62fbb897 Upgrade ProcessPageList to support showing and use of trash to non-superusers, for pages user is allowed to edit 2018-06-28 12:59:59 -04:00
Ryan Cramer
8e084a1ba0 Add new collapsed “What pages point to this one?” field on page editor Settings tab. This also demonstrates use of the $page->references() and $page->links(); methods. 2018-06-28 12:56:29 -04:00
Ryan Cramer
2883d2adb5 Add new $page->restorable() method to accompany the existing $page->trashable() method. This method returns true/false as to whether or not the page can be restored from the trash to its original location by the current user. Also add a PagePermissions::trashListable() method for determining whether the trash is viewable by the current user. 2018-06-28 12:53:56 -04:00
Ryan Cramer
54fad20ffd Add new Page methods: $page->links() which returns PageArray of other pages linking to it; $page->references() which returns PageArray of other pages referencing it in Page fields; $page->urls() which returns array of all URLs that can refer to the page, in all languages, and including both previous and current URLs. Plus add these properties: $page->references; $page->links; $page->numLinks; $page->numReferences; $page->hasLinks; $page->hasReferences; $page->referencing; $page->urls; see phpdoc for details on these properties. 2018-06-28 12:49:27 -04:00
Ryan Cramer
94524a8776 Add new find methods to FieldtypePage, FieldtypeTextarea and MarkupQA for finding page references and links specific to a given page. The public interfaces to these will be in the Page class (coming in next commit). 2018-06-28 12:47:05 -04:00
Ryan Cramer
5a9976f7a7 Improvements to User::hasPermission() method to support new permission detection options in context argument. 2018-06-28 12:43:09 -04:00
Ryan Cramer
8ef358eac7 Support option of forcing use of core WireMail even when other WireMail modules installed (primarily for testing/debugging purposes) 2018-06-28 12:42:03 -04:00
Ryan Cramer
bd325ba123 Fix issue where InputfieldImage could potentially detect ajax request mode when not applicable 2018-06-28 12:39:43 -04:00
Ryan Cramer
67bbd9641d Improvements to PagePathHistory module to support new options for getPathHistory() method 2018-06-28 12:38:24 -04:00
Ryan Cramer
fb65040836 Improvements to ProcessPageType and some phpdoc improvements to ProcessRole and ProcessPageLister 2018-06-28 12:35:18 -04:00
Ryan Cramer
cccc2d1161 Add lazy-loading option to WireInput, specified by $config->wireInputLazy=true; 2018-06-28 12:29:50 -04:00
Ryan Cramer
b54b3bace1 Add $modules->getModuleInfoProperty($module, 'name'); to retrieve just one property of module info, when useful. 2018-06-27 11:07:59 -04:00
Ryan Cramer
e5e13c1904 Fix issue processwire/processwire-issues#624 where WireHttp was producing error on the occasion when HTTP code present without text (which seems to be rare as far as I can tell). 2018-06-27 10:39:19 -04:00
Ryan Cramer
be1a203247 Fix issue processwire/processwire-issues#622 in $sanitizer->url() to workaround that PHP’s FILTER_VALIDATE_URL does not accept underscores in hostnames, despite their use being fairly common (even if not technically valid). 2018-06-27 10:24:59 -04:00
Ryan Cramer
1e912c4a4d Fix issue processwire/processwire-issues#623 where WireHttp::download() method was not working with URLs having encoded spaces when downloading with the "fopen" option (the "curl" option was not affected). Added a couple new $options to $sanitizer->url() method for dictating how encoded characters should be handled. Added WireHttp::setValidateURLOptions() method for cases where you want to manually specify different options for validating the URL in WireHttp. 2018-06-27 08:30:31 -04:00
Ryan Cramer
637f81579e Remove debug message() calls in ProcessPageEdit.module 2018-06-20 11:10:45 -04:00
Ryan Cramer
643c9d3a87 Bump version to 3.0.106 2018-06-15 13:19:15 -04:00
Ryan Cramer
afb4c4dbfd Add support for users with user-admin-all permission to be able to assign roles with user-admin permission per processwire/processwire-issues#607 2018-06-15 11:17:25 -04:00
Ryan Cramer
c82dba8835 Some upgrades and code consolidation in MarkupPagerNav, plus make it populate a $config->pagerHeadTags that one can output in a document head after rendering pagination, containing the <link rel='next|prev'.../> tags when desired. 2018-06-15 11:06:05 -04:00
Ryan Cramer
e60b79bd78 Upgrade $input->urlSegmentStr() method with new arguments, add an $input->pageNumStr() method, and add an $input->httpHostUrl() method that includes current scheme and hostname without path. 2018-06-15 11:03:38 -04:00
Ryan Cramer
28d275fe4d Fix issue processwire/processwire-issues#616 where PHP 7.2 is throwing deprecation notices for default arguments of idn_to_utf8() and idn_to_ascii(). The deprecation change and new defaults for next major PHP version does not appear to affect our usage after several tests, so opted to suppress these notices. 2018-06-15 07:58:50 -04:00
Ryan Cramer
828a80f1f6 Fix issue processwire/processwire-issues#621 where PageFrontEdit module config modal in AdminThemeDefault was showing extra/unnecessary buttons on modal dialog 2018-06-15 06:38:04 -04:00
Ryan Cramer
325ee3da15 Add a couple of new traversal methods to Inputfield: $inputfield->getRootParent(); and $inputfield->getForm();. While it was possible to get this info before, this makes it more obvious and straightforward. 2018-06-13 15:34:41 -04:00
Ryan Cramer
bd35c02e81 Improve support for Field tags by adding a new "Manage Tags" button at the bottom of the fields list screen, enabling you to add or remove fields to/from tags. In addition tags can now be used in $pages->find() searches, i.e. $pages->find("my_tag%=something"); would search all fields in the "my_tag" collection. 2018-06-13 15:31:55 -04:00
Ryan Cramer
a465fab672 Several fixes and impovements to PageFrontEdit per processwire/processwire-issues#609. Plus, this commit also improves PageFrontEdit usage with AdminThemeUikit, and improves behavior of multi-language fields. 2018-06-12 07:43:55 -04:00
Ryan Cramer
738c2db3c5 Correct issue in JqueryUI/modal.js where it used $() rather than jQuery() in one spot, and also fix issue where modal would stay open in some cases when we didn't want it to 2018-06-12 07:12:04 -04:00
Ryan Cramer
9ac3bfb5ad Various small code improvements and optimizations to ProcessPageEdit.module 2018-06-12 07:09:35 -04:00
Ryan Cramer
fff1250211 Various minor updates: add a $config->version($minVersion) function, add @since tags in Sanitizer, update count() => wireCount() in FieldtypePageTable and add PHPdoc hints where phpstorm is asking for them, update InputfieldImage to track change for the field when an action manipulates the image file, plus some other minor odds and ends. 2018-06-12 07:06:40 -04:00
Ryan Cramer
9ef1ac1486 Fix issue where drag-to-upload to CKE field within a repeater wasn't working quite right. Also updated it to default to drag-upload to image field in repeater item, if one is present (and supports more than one item), and use the owning page as a fallback for upload. 2018-06-10 08:08:53 -04:00
Ryan Cramer
d57b3ffcfc Bump version to 3.0.105 2018-06-01 15:39:56 -04:00
Ryan Cramer
6ef23f5c75 A couple of minor updates to AdminThemeUikit 2018-06-01 14:08:19 -04:00
Ryan Cramer
f6d8d3510f Add the “Regular” site profile to the core as another installation profile option 2018-06-01 13:26:07 -04:00
Ryan Cramer
ecf812224f Add $mail->sendHTML() and $mail->mailHTML() methods which are the same as their send() and mail() counterparts, but assume the given message body is HTML, and text-body is auto-generated. 2018-06-01 09:17:37 -04:00
Ryan Cramer
c0437702ce Fix issue processwire/processwire-issues#610 where .sort-date spans were showing in MarkupAdminDataTable columns at mobile width 2018-06-01 06:55:50 -04:00
Ryan Cramer
2abc454ce1 Add @rolandtoth PR #110 and PR #111 that fix issues with LanguageTabs in AdminThemeDefault/AdminThemeReno themes. 2018-05-31 14:55:33 -04:00
Ryan Cramer
07ab8ef9fc Add support for multi-language options support to InputfieldSelect, and modules that descend from it (radios, checkboxes, asmSelect, selectMultiple, etc). This enables you to use selectable options in multiple languages when used in FormBuilder or any other tool that uses InputfieldSelect without a Fieldtype. 2018-05-31 14:27:39 -04:00
Ryan Cramer
d2da3adcf7 Add new $mail->mail() method, which is the same as the existing $mail->send() method except that its arguments duplicate those of PHP mail(), making it possible to use as a drop-in replacement for PHP mail(). 2018-05-31 13:17:22 -04:00
Ryan Cramer
7c159f9c1a Some reworking of and improvements to the WireMail class, though no change to external interface (and should be no changes required for classes extending it). 2018-05-31 13:15:32 -04:00
Ryan Cramer
863abee8bb mprovements and new options for $sanitizer methods: text(), textarea(), unentities(). Plus, add new $sanitizer->removeWhitespace() method that can remove 99 different types of whitespace that can appear in a string (which includes the obvious whitespace, but also various UTF-8 and html-entity based whitespace). 2018-05-31 11:07:36 -04:00
Ryan Cramer
46e774ba38 Fix issue in FieldtypeComments::find() where "sort=something" selector wasn't always working if DatabaseQuerySelectFulltext modified the query 2018-05-30 07:29:00 -04:00
Ryan Cramer
bdaf8810cb Various minor AdminThemeUikit updates, mostly styles adjustments. Bump version to 3.0.104 and a couple other minor tweaks. 2018-05-25 11:10:41 -04:00
Ryan Cramer
5b38c26806 Update InputfieldCKEditor figure/figcaption styles to bind width of caption to width of image 2018-05-25 10:40:15 -04:00
Ryan Cramer
81ceb184b2 Update FieldtypeComments CommentList class to support a footer option 2018-05-25 10:38:40 -04:00
Ryan Cramer
ad9973ca33 Additional fix for processwire/processwire-issues#602 add z-index to InputfieldRepeater to prevent lost outline at bottom of field when repeater depth enabled 2018-05-24 05:47:56 -04:00
Ryan Cramer
da73572b95 Fix issue in Firefox where repeaters with depth in AdminThemeUikit had strange outline on containing Inputfield element per suggestion in processwire/processwire-issues#602 2018-05-24 05:45:21 -04:00
Ryan Cramer
c1dc6a9b9f Add @Notanotherdotcom PR #96 to allow use of field names with brackets (array) in field dependencies 2018-05-23 08:34:33 -04:00
Ryan Cramer
d06fd64171 Fix issue processwire/processwire-issues#603 where toggling repeater item open/closed makes outline disappear the second time you do it 2018-05-23 08:28:54 -04:00
Ryan Cramer
c512fd68e3 Fix issue processwire/processwire-issues#598 in FieldtypeOptions where manually assigned option IDs were not working 2018-05-23 07:45:33 -04:00
Ryan Cramer
91930953b6 Additional update for processwire/processwire-issues#596 2018-05-23 05:43:08 -04:00
Ryan Cramer
af3db8b5c1 Fix issue processwire/processwire-issues#600 where label saved to log was showing wrong path 2018-05-21 09:45:24 -04:00
Ryan Cramer
771c275432 Fix issue processwire/processwire-issues#597 allow radio button labels to wrap when needed 2018-05-21 09:40:35 -04:00
Ryan Cramer
79fa81d8a8 Add enhancement processwire/processwire-issues#596 show field labels rather than field names in Inputfield error messages 2018-05-21 09:23:47 -04:00
Ryan Cramer
2ba8d29ce2 Fix issue processwire/processwire-issues#595 where horizontal separator in dropdown nav wasn't quite right for the Page Add > Bookmarks option 2018-05-21 08:47:04 -04:00
Ryan Cramer
76a15a2539 Some other tweaks to Fieldtype and FieldtypeComments and bump version to 3.0.103 2018-05-18 15:07:24 -04:00
Ryan Cramer
9c38d29402 Improve support for fields that can be placed in custom header of CommentList class 2018-05-18 10:15:04 -04:00
Ryan Cramer
0fbd5a5882 Significant improvements to FieldtypeComments::find() method, adding support for selectors containing multiple sort values, OR values (for most properties), fulltext index searching of comment text, and matching comments based on field values present on the page that contains the comment field. 2018-05-18 10:12:04 -04:00
Ryan Cramer
aa34db127e Add WireLog option for specifying which user should be recorded with the log entry (if different from current user). 2018-05-18 09:21:13 -04:00
Ryan Cramer
7b2692f0cd Add new removeAllFor($namespace) method to Session, and improve the multi-IP option in the getIP() method 2018-05-18 09:19:01 -04:00
Ryan Cramer
ac480032c6 Major improvements and configuration options added to ProcessForgotPassword module. Also improves it for non-admin uses such as with the LoginRegister module. Additional added hooks also support more customization. 2018-05-18 09:17:38 -04:00
Ryan Cramer
8dda4368ea Bump version to 3.0.102 2018-05-11 12:00:35 -04:00
Ryan Cramer
8b285cfb17 Add @kixe PR #94 which adds a "disallow parallel sessions" option to SessionHandlerDB. When enabled, only one login can be maintained at a time per user. 2018-05-11 09:52:46 -04:00
Ryan Cramer
63cba339e4 Add "Cancel" JS callback to ProcessWire.confirm(message, funcOk, funcCancel) per PR #108 2018-05-11 08:20:26 -04:00
Ryan Cramer
717c1d2961 Add PR #107 so that panel.js triggers pw-panel-closed and pw-panel-opened events in JS 2018-05-11 06:33:33 -04:00
Ryan Cramer
8ef088f625 Some minor updates to WireTextTools::truncate() method to improve sentence type matches 2018-05-11 06:14:15 -04:00
Ryan Cramer
d7e0cccecb Apply the LanguageSupportPageNames duplicate checking feature without debug/advanced modes per processwire/processwire-issues#322 2018-05-10 06:25:30 -04:00
Ryan Cramer
7458d0504c Make InputfieldSelect show blank option even when selection is required per processwire/processwire-issues#585 2018-05-10 06:02:57 -04:00
Ryan Cramer
04123e305a Fix issue processwire/processwire-issues#588 where InputfieldFieldsetOpen lacked class InputfieldFieldset, which prevented it from having background color in AdminThemeUikit 2018-05-10 05:52:40 -04:00
Ryan Cramer
2d8dc82010 Fix issue processwire/processwire-issues#590 dropdown hover ajax menus using fix suggested in issue report 2018-05-10 05:37:29 -04:00
Ryan Cramer
e520c09f7a Update InputfieldPageAutocomplete.js to initialize the autocomplete on focus event, rather than on document ready. This resolves a render time issue in AdminThemeUikit when there are a lot of autocomplete inputs present, per @Toutouwai 2018-05-08 09:25:07 -04:00
Ryan Cramer
2f20fe402c Some code cleanup in ProcessPageSearch, plus add a hook for processwire/processwire-issues#584 2018-05-08 08:48:52 -04:00
Ryan Cramer
5127be3b35 Fix issue processwire/processwire-issues#587 where "Pages > Recent > Created" dropdown was showing pages created by user rather than all users. 2018-05-08 08:09:13 -04:00
Ryan Cramer
211cf78403 Fix issue processwire/processwire-issues#586 unnecessary "clone_field" property in field settings after a field is cloned. Unrelated: this commit also converts InputfieldPageAutocomplete to use an scss file. 2018-05-08 08:03:41 -04:00
Ryan Cramer
a3d2a447a9 Add fix for issue processwire/processwire-issues#584 allow quotes in InputfieldPageAutocomplete selectors 2018-05-08 05:43:24 -04:00
Ryan Cramer
ebe163452d Bump version to 3.0.101 2018-05-04 15:35:01 -04:00
Ryan Cramer
8d38ad7c44 Add $sanitizer->truncate() function per processwire/processwire-requests#163 2018-05-04 15:32:54 -04:00
Ryan Cramer
589d745caa Some small CSS fixes in ProcessModule 2018-05-02 06:24:49 -04:00
Ryan Cramer
c2bc83bc40 Attempt fix for processwire/processwire-issues#582 requiredIf and ProcessPageEdit behaviors 2018-05-02 06:14:47 -04:00
Ryan Cramer
027275ec40 Fix issue processwire/processwire-issues#583 $pages->clone() was failing when page had files and output formatting state was true. 2018-05-02 05:53:26 -04:00
Ryan Cramer
3296b699e2 Fix issue in InputfieldImage where negative degree rotate image actions were getting converted to positive degree rotates due to sanitization. 2018-05-01 06:09:21 -04:00
Ryan Cramer
8c18e3bf0d Fix issue processwire/processwire-issues#580 2018-04-30 05:56:36 -04:00
Ryan Cramer
ecdd3a3895 Bump version to 3.0.100 2018-04-27 16:34:17 -04:00
Ryan Cramer
cfed957df2 Update $pages->newPage() method to support option of accepting array of fields/properties to set when creating the page object 2018-04-27 11:21:13 -04:00
Ryan Cramer
b0abd3b9de Additional update for processwire/processwire-issues#575 2018-04-27 10:28:13 -04:00
Ryan Cramer
43c3bb52c3 Additional adjustment for processwire/processwire-requests#183 to correct enter-key detection that was using wrong element id. 2018-04-27 10:13:50 -04:00
Ryan Cramer
904db74f1a Add custom __debugInfo() methods to Pagefile, Pageimage, and Pagefiles per processwire/processwire-issues#575 plus some other minor updates to Pagefile/Pageimage 2018-04-27 09:55:31 -04:00
Ryan Cramer
c2324f0a8a Fix issue processwire/processwire-issues#576 adjustment in InputfieldPassword.js for EmailNewUser module 2018-04-27 06:09:07 -04:00
Ryan Cramer
2a49fe1087 Attempt to fix issue processwire/processwire-issues#560 add support for viewable permission with User pages outside admin structure when developer intends it 2018-04-26 10:04:49 -04:00
Ryan Cramer
105ba2b8eb Add __debugInfo() method to WireInput class per processwire/processwire-issues#575 2018-04-26 08:48:09 -04:00
Ryan Cramer
e53e4b4bd0 Add processwire/processwire-requests#183 focus URL input field when ProcessPageEditLink opens 2018-04-26 08:32:42 -04:00
Ryan Cramer
2cc69a1f0b Fix issue processwire/processwire-issues#571 where configuring a FieldtypeFieldsetClose in Setup > Fields would result in error in AdminThemeUikit 2018-04-25 11:16:05 -04:00
Ryan Cramer
c7dfb37a7e Fix issue processwire/processwire-issues#570 where InputfieldImage in a Repeater item, crop/focus/variations buttons didn't show to non-superusers 2018-04-25 11:09:59 -04:00
Ryan Cramer
d691b743b0 Fix issue processwire/processwire-issues#569 when Inputfield::textFormatMarkdown constant used it wasn't replacing default wrapping markup 2018-04-25 09:07:18 -04:00
Ryan Cramer
ba400767ab Fix issue processwire/processwire-issues#568 for ProcessModule table.AdminDataList tables, plus convert CSS to SCSS 2018-04-25 08:21:47 -04:00
Ryan Cramer
ace36fb415 Fix issue processwire/processwire-issues#567 w/AdminThemeUikit search box ctrl/cmd-click did not open new tab 2018-04-25 06:12:50 -04:00
Ryan Cramer
a9051a252f Fix issue processwire/processwire-issues#565 with InputfieldSelector preview count and _custom selector option was not working before 2018-04-25 05:55:22 -04:00
Ryan Cramer
f2912bcfd4 A couple of minor adjustments in AdminThemeUikit 2018-04-23 05:42:12 -04:00
Ryan Cramer
184059b5d6 Additional improvements to $config->noHTTPS option for support of hostnames 2018-04-20 10:33:43 -04:00
Ryan Cramer
bc037f840e Minor adjustment to InputfieldRepeater and publish status to simplify a ProDrafts hook 2018-04-19 10:52:08 -04:00
Ryan Cramer
16c20439f1 Add support for $config->noHTTPS. When boolean true, it cancels any HTTPS requirements set per-template, simplifying cases where you copy a production site to dev site that may not have the same HTTPS capabilities. So for that case, the dev site would want to have $config->noHTTPS=true; in /site/config.php 2018-04-19 10:49:58 -04:00
Ryan Cramer
a6bd85f6b2 Some additional updates for AdminThemeUikit to improve on irregular Inputfield column widths and global uk-form-large or uk-form-small settings 2018-04-19 10:47:13 -04:00
Ryan Cramer
19f557b487 Several updates to AdminThemeUikit including additional config settings, new input appearance adjustments, option to use percentage-based widths rather than uk-width classes, and more. processwire/processwire-issues#564 processwire/processwire-issues#480 2018-04-18 09:05:29 -04:00
Ryan Cramer
c550c6678b Some minor adjustments in preparation for a couple AdminThemeUikit additions 2018-04-18 08:50:53 -04:00
Ryan Cramer
d2ebb4cd56 Update base AdminTheme class for more flexible class management functions 2018-04-18 08:47:20 -04:00
Ryan Cramer
566d60b152 Small optimization for for __('text') translation function when used in PHP 5.4.0 or newer and no textdomain specified. 2018-04-13 09:17:09 -04:00
Ryan Cramer
5884f05b9f Fix issue in MarkupFieldtype when rendering file/image fields within repeater items 2018-04-13 08:50:02 -04:00
Ryan Cramer
6b3b0b7782 Further updates to AdminThemeUikit Inputfield width adjustments per processwire/processwire-issues#480 2018-04-13 08:31:17 -04:00
Ryan Cramer
ae5b955ec8 Fix issue in FieldtypeFieldsetOpen.module where hooks might not get called depending on module load order 2018-04-08 08:17:09 -04:00
Ryan Cramer
341342dc5b Bump version to 3.0.98 2018-04-06 14:25:49 -04:00
Ryan Cramer
9d11d87e9b Update InputfieldSelect::addOptionsString() to allow for user-defined disabled option if line in option string is prefixed with "disabled:" 2018-04-06 07:14:48 -04:00
Ryan Cramer
c5147a5279 Make Inputfield requiredLabel customizable per field from API via $inputfield->requiredLabel property, to provide option to override default "missing requird value" label. 2018-04-06 06:56:54 -04:00
Ryan Cramer
f27feb2bbf Followup fix for issue processwire/processwire-issues#322 check for duplicate non-default language page name when creating new page 2018-04-06 06:46:42 -04:00
Ryan Cramer
eaa2cb4d89 Fix issue processwire/processwire-issues#558 remove unused filemtime() call in ProcessLanguage.module 2018-04-06 05:46:30 -04:00
Ryan Cramer
215348b797 Fix issue processwire/processwire-issues#556 fix issue when cloning Pagefile that has name ending with "-0". 2018-04-06 05:43:25 -04:00
Ryan Cramer
6dc7a122a6 Fix issue processwire/proceswire-issues#555 in ProcessProfile where 50% width Inputfield followed by submit button resulted in submit button in wrong position 2018-04-06 05:37:22 -04:00
Ryan Cramer
c5c7ae4232 Bump to 3.0.97 and update to add a Pagefile::HTTPURL to accompany the existing Pagefile::URL property (uppercase implies cache busting URLs). 2018-03-30 10:48:40 -04:00
Ryan Cramer
2722345aac Attempt to fix issue processwire/processwire-issues#554 isue with $files->include() and Windows paths 2018-03-30 10:45:35 -04:00
Ryan Cramer
b1214fa0c4 Fix issue processwire/processwire-issues#553 where a note in AdminThemeUikit config screen needed to be updated for new module location 2018-03-30 09:53:46 -04:00
Ryan Cramer
8cbe05d1e7 Fix issue processwire/processwire-issues#277 where Inputfield::collapsedNever constant didn't work quite right in some cases 2018-03-30 09:41:32 -04:00
Ryan Cramer
deb448851b Fix issue processwire/processwire-issues#544 where FieldsetTab in User editor was not viewable to non-superuser with user-admin permission 2018-03-28 09:43:44 -04:00
Ryan Cramer
b7e90f896c Fix issue processwire/processwire-issues#551 Pages::trashed and Pages::restored hooks were getting called twice 2018-03-28 06:31:36 -04:00
Ryan Cramer
164d132be8 Fix issue processwire/processwire-issues#548 WireLog::getLines method wasn't passing options array to FileLog::find 2018-03-28 05:59:40 -04:00
Ryan Cramer
bd85c025e2 Fix issue in ProcessCommentsManager where the tabs were not showing active state properly in AdminThemeUikit. Plus some minor CSS updates in InputfieldPage and ProcessPageLister. 2018-03-22 07:59:16 -04:00
Ryan Cramer
a5e5ae7b47 Update FieldtypePage to support a runtime option to create Page references that don't exist when value is set by title. This is to accommodate a feature in ImportPagesCSV module. 2018-03-22 07:48:26 -04:00
Ryan Cramer
3e6840748b Update FieldtypeInteger so that it will still convert to integer even if number prefixed with USD or EUR symbol. 2018-03-22 07:46:02 -04:00
Ryan Cramer
89ed62a6d8 Prevent stars input from showing in InputfieldCommentsAdmin.module when stars feature is not enabled 2018-03-22 07:43:47 -04:00
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
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
1264 changed files with 112712 additions and 10701 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

@@ -205,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]

View File

@@ -11,7 +11,7 @@
* If that file exists, the installer will not run. So if you need to re-run this installer for any
* reason, then you'll want to delete that file. This was implemented just in case someone doesn't delete the installer.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
* @todo have installer set session name
@@ -57,7 +57,7 @@ class Installer {
/**
* File permissions, determined in the dbConfig function
*
* Below are last resort defaults
* Below are worst case scenario, last resort defaults
*
*/
protected $chmodDir = "0777";
@@ -67,7 +67,15 @@ class Installer {
* Number of errors that occurred during the request
*
*/
protected $numErrors = 0;
protected $numErrors = 0;
/**
* True when we are in a section
*
* @var bool
*
*/
protected $inSection = false;
/**
* Available color themes
@@ -91,11 +99,11 @@ class Installer {
}
// these two vars used by install-head.inc
$title = "ProcessWire " . PROCESSWIRE_INSTALL . " Installation";
$title = "ProcessWire " . PROCESSWIRE_INSTALL . " Installer";
$formAction = "./install.php";
if($title && $formAction) {} // ignore
require("./wire/modules/AdminTheme/AdminThemeDefault/install-head.inc");
require("./wire/modules/AdminTheme/AdminThemeUikit/install-head.inc");
if(isset($_POST['step'])) switch($_POST['step']) {
@@ -109,6 +117,7 @@ class Installer {
case 5: require("./index.php");
/** @var ProcessWire $wire */
$wire->modules->refresh();
$this->adminAccountSave($wire);
break;
@@ -117,7 +126,7 @@ class Installer {
} else $this->welcome();
require("./wire/modules/AdminTheme/AdminThemeDefault/install-foot.inc");
require("./wire/modules/AdminTheme/AdminThemeUikit/install-foot.inc");
}
@@ -199,6 +208,7 @@ class Installer {
$out = '';
$profiles = $this->findProfiles();
if(!count($profiles)) $this->err("No profiles found!");
foreach($profiles as $name => $profile) {
$title = empty($profile['title']) ? ucfirst($profile['name']) : $profile['title'];
//$selected = $name == 'site-default' ? " selected='selected'" : "";
@@ -218,15 +228,18 @@ class Installer {
echo "
<p>A site installation profile is a ready-to-use and modify site for ProcessWire.
If you are just getting started with ProcessWire, we recommend choosing
the <em>Default</em> site profile. If you already know what you are doing,
If you are just getting started with ProcessWire, we recommend choosing the <em>Regular</em>
or <em>Default</em> site profile. If you already know what you are doing,
you might prefer the <em>Blank</em> site profile.
<p>
<select name='profile' id='select-profile'>
<option value=''>Installation Profiles</option>
$options
</select>
<span class='detail'><i class='fa fa-angle-left'></i> Select each installation profile to see more information and a preview.</span>
<p style='width: 240px;'>
<select class='uk-select' name='profile' id='select-profile'>
<option value=''>Installation Profiles</option>
$options
</select>
</p>
<p class='detail'>
<i class='fa fa-angle-up'></i>
Select each installation profile to see more information and a preview.
</p>
$out
<script type='text/javascript'>
@@ -245,20 +258,20 @@ class Installer {
*/
protected function initProfile() {
$this->h('Site Installation Profile');
$this->h('Site Installation Profile', 'building-o');
if(is_file("./site/install/install.sql")) {
$this->ok("Found installation profile in /site/install/");
$this->alertOk("Found installation profile in /site/install/");
} else if(is_dir("./site/")) {
$this->ok("Found /site/ -- already installed? ");
$this->alertOk("Found /site/ already installed? ");
} else if(isset($_POST['profile'])) {
$profiles = $this->findProfiles();
$profile = preg_replace('/[^-a-zA-Z0-9_]/', '', $_POST['profile']);
if(empty($profile) || !isset($profiles[$profile]) || !is_dir(dirname(__FILE__) . "/$profile")) {
$this->err("Profile not found");
$this->alertErr("Profile not found");
$this->selectProfile();
$this->btn("Continue", 0);
return;
@@ -267,9 +280,9 @@ class Installer {
// $this->h(empty($info['title']) ? ucfirst($info['name']) : $info['title']);
if(@rename("./$profile", "./site")) {
$this->ok("Renamed /$profile => /site");
$this->alertOk("Renamed /$profile => /site");
} else {
$this->err("File system is not writable by this installer. Before continuing, please rename '/$profile' to '/site'");
$this->alertErr("File system is not writable by this installer. Before continuing, please rename '/$profile' to '/site'");
$this->btn("Continue", 0);
return;
}
@@ -289,7 +302,7 @@ class Installer {
*/
protected function compatibilityCheck() {
$this->h("Compatibility Check");
$this->sectionStart('fa-gears Compatibility Check');
if(version_compare(PHP_VERSION, self::MIN_REQUIRED_PHP_VERSION) >= 0) {
$this->ok("PHP version " . PHP_VERSION);
@@ -370,6 +383,7 @@ class Installer {
} else {
$this->ok(".htaccess looks good");
}
$this->sectionStop();
if($this->numErrors) {
$this->p("One or more errors were found above. We recommend you correct these issues before proceeding or <a href='http://processwire.com/talk/'>contact ProcessWire support</a> if you have questions or think the error is incorrect. But if you want to proceed anyway, click Continue below.");
@@ -384,16 +398,36 @@ class Installer {
* Step 2: Configure the database and file permission settings
*
* @param array $values
* @param int $hasNumTables
*
*/
protected function dbConfig($values = array()) {
if(!is_file("./site/install/install.sql")) die("There is no installation profile in /site/. Please place one there before continuing. You can get it at processwire.com/download");
protected function dbConfig($values = array(), $hasNumTables = 0) {
if(!is_file("./site/install/install.sql")) die(
"There is no installation profile in /site/. Please place one there before continuing. " .
"You can get it at https://processwire.com/download/"
);
$this->h("MySQL Database");
$this->p("Please specify a MySQL 5.x database and user account on your server. If the database does not exist, we will attempt to create it. If the database already exists, the user account should have full read, write and delete permissions on the database.*");
$this->p("*Recommended permissions are select, insert, update, delete, create, alter, index, drop, create temporary tables, and lock tables.", "detail");
if($hasNumTables) {
$this->sectionStart('fa-database Existing tables action');
// select($name, $label, $value, array $options) {
$this->p("What would you like to do with the existing database tables that are present?");
$this->select('dbTablesAction', '', 0, array(
'0' => "Click to select tables action",
'ignore' => "Ignore tables*",
'remove' => "Remove tables",
), 0);
$this->p("*When choosing “Ignore tables”, existing tables having the same name as newly imported tables will still be deleted.", 'detail');
$this->sectionStop();
}
$this->sectionStart('fa-database MySQL Database');
$this->p(
"Please specify a MySQL 5.x+ database and user account on your server. If the database does not exist, " .
"we will attempt to create it. If the database already exists, the user account should have full read, " .
"write and delete permissions on the database (recommended permissions are select, insert, update, delete, " .
"create, alter, index, drop, create temporary tables, and lock tables)."
);
if(!isset($values['dbName'])) $values['dbName'] = '';
// @todo: are there PDO equivalents for the ini_get()s below?
@@ -404,8 +438,10 @@ class Installer {
if(!isset($values['dbEngine'])) $values['dbEngine'] = 'MyISAM';
if(!$values['dbHost']) $values['dbHost'] = 'localhost';
if(!$values['dbPort']) $values['dbPort'] = 3306;
if(!$values['dbPort']) $values['dbPort'] = 3306;
if(empty($values['dbCharset'])) $values['dbCharset'] = 'utf8';
if($values['dbCharset'] != 'utf8mb4') $values['dbCharset'] = 'utf8';
if($values['dbEngine'] != 'InnoDB') $values['dbEngine'] = 'MyISAM';
foreach($values as $key => $value) {
if(strpos($key, 'chmod') === 0) {
@@ -415,39 +451,22 @@ class Installer {
}
}
$this->input('dbName', 'DB Name', $values['dbName']);
$this->input('dbUser', 'DB User', $values['dbUser']);
$this->input('dbPass', 'DB Pass', $values['dbPass'], false, 'password', false);
$this->input('dbHost', 'DB Host', $values['dbHost']);
$this->input('dbPort', 'DB Port', $values['dbPort'], true);
echo
"<div id='dbAdvancedToggle'><small>" .
"<a class='ui-priority-secondary' href='#' onclick='$(\"#dbAdvanced\").slideDown();$(\"#dbAdvancedToggle\").slideUp();'>" .
"<i class='fa fa-wrench'></i> Advanced: Charset &amp; Engine &hellip;</a>" .
"</small></div>";
echo "<div id='dbAdvanced' style='display: none'>";
$this->h('Advanced Database Options');
$this->input('dbHost', 'DB Host', $values['dbHost'], false);
$this->input('dbPort', 'DB Port', $values['dbPort']);
$this->select('dbCharset', 'DB Charset', $values['dbCharset'], array('utf8', 'utf8mb4'));
$this->select('dbEngine', 'DB Engine', $values['dbEngine'], array('MyISAM', 'InnoDB'));
$this->clear();
$this->p(
"The 'utf8' and 'MyISAM' options are known to work across the broadest range of servers and 3rd party modules, " .
"so you should not change these settings unless you know what you are doing. " .
"The 'utf8mb4' (charset) and/or 'InnoDB' (engine) may be preferable for some installations. " .
"*Please note the 'InnoDB' option requires MySQL 5.6.4 or newer."
"The DB Charset option “utf8mb4” may not be compatible with all 3rd party modules.<br />" .
"The DB Engine option “InnoDB” requires MySQL 5.6.4 or newer.",
array('class' => 'detail', 'style' => 'margin-top:0')
);
echo "<p style='width: 135px; float: left; margin-top: 0;'><label>DB Charset</label><br />";
echo "<select name='dbCharset'>";
echo "<option value='utf8'" . ($values['dbCharset'] != 'utf8mb4' ? " selected" : "") . ">utf8</option>";
echo "<option value='utf8mb4'" . ($values['dbCharset'] == 'utf8mb4' ? " selected" : "") . ">utf8mb4</option>";
echo "</select></p>";
// $this->input('dbCharset', 'DB Charset', $values['dbCharset']);
echo "<p style='width: 135px; float: left; margin-top: 0;'><label>DB Engine</label><br />";
echo "<select name='dbEngine'>";
echo "<option value='MyISAM'" . ($values['dbEngine'] != 'InnoDB' ? " selected" : "") . ">MyISAM</option>";
echo "<option value='InnoDB'" . ($values['dbEngine'] == 'InnoDB' ? " selected" : "") . ">InnoDB*</option>";
echo "</select></p>";
echo "</div>";
$this->sectionStop();
$cgi = false;
$defaults = array();
@@ -484,18 +503,12 @@ class Installer {
$values = array_merge($defaults, $values);
$this->h("Default Time Zone");
echo "<p><select name='timezone'>";
foreach($this->timezones() as $key => $timezone) {
$label = $timezone;
if(strpos($label, '|')) list($label, $timezone) = explode('|', $label);
$selected = $timezone == $values['timezone'] ? "selected='selected'" : '';
$label = str_replace('_', ' ', $label);
echo "<option value=\"$key\" $selected>$label</option>";
}
echo "</select></p>";
$this->sectionStart('fa-globe Time Zone');
$this->p('The time zone selection should be consistent with the time zone of the web server you are installing to.');
$this->selectTimezone($values['timezone']);
$this->sectionStop();
$this->h("File Permissions");
$this->sectionStart("fa-key File Permissions");
$this->p(
"When ProcessWire creates directories or files, it assigns permissions to them. " .
"Enter the most restrictive permissions possible that give ProcessWire (and you) read and write access to the web server (Apache). " .
@@ -510,19 +523,27 @@ class Installer {
$this->input('chmodFile', 'Files', $values['chmodFile'], true);
if($cgi) {
echo "<p class='detail' style='margin-top: 0;'>We detected that this file (install.php) is writable. That means Apache may be running as your user account. Given that, we populated the permissions above (755 &amp; 644) as possible starting point.</p>";
$this->p(
"We detected that this file (install.php) is writable. That means Apache may be running as your user account. Given that, we populated the permissions above (755 &amp; 644) as possible starting point.",
array('class' => 'detail', 'style' => 'margin-top:0')
);
} else {
echo "<p class='detail' style='margin-top: 0;'>WARNING: 777 and 666 permissions mean that directories and files are readable and writable to everyone on the server (and thus not particularly safe). If in any kind of shared hosting environment, please consult your web host for their recommended permission settings for Apache readable/writable directories and files before proceeding. <a target='_blank' href='https://processwire.com/docs/security/file-permissions/'>More</a></p>";
$this->p(
"WARNING: 777 and 666 permissions mean that directories and files are readable and writable to everyone on the server (and thus not particularly safe). If in any kind of shared hosting environment, please consult your web host for their recommended permission settings for Apache readable/writable directories and files before proceeding. " .
"<a target='_blank' href='https://processwire.com/docs/security/file-permissions/'>More</a>",
array('class' => 'detail', 'style' => 'margin-top:0')
);
}
$this->sectionStop();
$this->h("HTTP Host Names");
$this->sectionStart('fa-server HTTP Host Names');
$this->p("What host names will this installation run on now and in the future? Please enter one host per line. You may also choose to leave this blank to auto-detect on each request, but we recommend using this whitelist for the best security in production environments.");
$this->p("This field is recommended but not required. You can set this later by editing the file <u>/site/config.php</u> (setting \$config->httpHosts).", "detail");
$rows = substr_count($values['httpHosts'], "\n") + 2;
echo "<p><textarea name='httpHosts' rows='$rows' style='width: 100%;'>" . htmlentities($values['httpHosts'], ENT_QUOTES, 'UTF-8') . "</textarea></p>";
$this->textarea('httpHosts', '', $values['httpHosts'], $rows);
$this->sectionStop();
$this->btn("Continue", 4);
$this->p("Note: After you click the button above, be patient &hellip; it may take a minute.", "detail");
}
@@ -539,7 +560,7 @@ class Installer {
$fields = array('chmodDir', 'chmodFile');
foreach($fields as $field) {
$value = (int) $_POST[$field];
if(strlen("$value") !== 3) $this->err("Value for '$field' is invalid");
if(strlen("$value") !== 3) $this->alertErr("Value for '$field' is invalid");
else $this->$field = "0$value";
$values[$field] = $value;
}
@@ -571,6 +592,7 @@ class Installer {
// db configuration
$fields = array('dbUser', 'dbName', 'dbPass', 'dbHost', 'dbPort', 'dbEngine', 'dbCharset');
foreach($fields as $field) {
$value = get_magic_quotes_gpc() ? stripslashes($_POST[$field]) : $_POST[$field];
$value = substr($value, 0, 255);
@@ -580,11 +602,10 @@ class Installer {
$values['dbCharset'] = ($values['dbCharset'] === 'utf8mb4' ? 'utf8mb4' : 'utf8');
$values['dbEngine'] = ($values['dbEngine'] === 'InnoDB' ? 'InnoDB' : 'MyISAM');
// if(!ctype_alnum($values['dbCharset'])) $values['dbCharset'] = 'utf8';
if(!$values['dbUser'] || !$values['dbName'] || !$values['dbPort']) {
$this->err("Missing database configuration fields");
$this->alertErr("Missing database configuration fields");
} else {
@@ -606,8 +627,8 @@ class Installer {
$database = $this->dbCreateDatabase($dsn, $values, $driver_options);
} else {
$this->err("Database connection information did not work.");
$this->err($e->getMessage());
$this->alertErr("Database connection information did not work.");
$this->alertErr(htmlentities($e->getMessage(), ENT_QUOTES, 'UTF-8'));
}
}
}
@@ -617,24 +638,55 @@ class Installer {
return;
}
$this->h("Test Database and Save Configuration");
$this->ok("Database connection successful to " . htmlspecialchars($values['dbName']));
$this->h("fa-database Test Database and Save Configuration");
$this->alertOk("Database connection successful to " . htmlspecialchars($values['dbName']));
$options = array(
'dbCharset' => strtolower($values['dbCharset']),
'dbEngine' => $values['dbEngine']
);
// check if MySQL is new enough to support InnoDB with fulltext indexes
if($options['dbEngine'] == 'InnoDB') {
$query = $database->query("SELECT VERSION()");
list($dbVersion) = $query->fetch(\PDO::FETCH_NUM);
if(version_compare($dbVersion, "5.6.4", "<")) {
$options['dbEngine'] = 'MyISAM';
$values['dbEngine'] = 'MyISAM';
$this->err("Your MySQL version is $dbVersion and InnoDB requires 5.6.4 or newer. Engine changed to MyISAM.");
$this->alertErr("Your MySQL version is $dbVersion and InnoDB requires 5.6.4 or newer. Engine changed to MyISAM.");
}
}
// check if database already has tables present
$query = $database->query("SHOW TABLES");
$tables = $query->fetchAll(\PDO::FETCH_COLUMN);
$numTables = count($tables);
$dbTablesAction = isset($_POST['dbTablesAction']) ? $_POST['dbTablesAction'] : '';
if($numTables && $dbTablesAction) {
if($dbTablesAction === 'remove') {
// remove
foreach($tables as $table) {
$database->exec("DROP TABLE `$table`");
}
$this->alertOk("Dropped $numTables existing table(s)");
$numTables = 0;
} else if($dbTablesAction === 'ignore') {
// ignore
$this->alertOk('Existing tables will be ignored');
} else {
$dbTablesAction = '';
}
}
if($this->dbSaveConfigFile($values)) {
if($numTables && empty($dbTablesAction)) {
$this->alertErr(
"<strong>Database already has $numTables table(s) present:</strong> " .
implode(', ', $tables) . ". " .
"<strong>Please select below what you would like to do with these tables.</strong>"
);
$this->dbConfig($values, $numTables);
} else if($this->dbSaveConfigFile($values)) {
$this->profileImport($database, $options);
} else {
$this->dbConfig($values);
@@ -673,17 +725,17 @@ class Installer {
$database->exec("CREATE SCHEMA IF NOT EXISTS `$dbName` DEFAULT CHARACTER SET `$dbCharset`");
// reconnect
$database = new \PDO($dsn, $values['dbUser'], $values['dbPass'], $driver_options);
if($database) $this->ok("Created database: $dbName");
if($database) $this->alertOk("Created database: $dbName");
} catch(\Exception $e) {
$this->err("Failed to create database with name $dbName");
$this->err($e->getMessage());
$this->alertErr("Failed to create database with name $dbName");
$this->alertErr($e->getMessage());
$database = null;
}
} else {
$database = null;
$this->err("Unable to create database with that name. Please create the database with another tool and try again.");
$this->alertErr("Unable to create database with that name. Please create the database with another tool and try again.");
}
return $database;
@@ -736,7 +788,13 @@ class Installer {
"\n * Installer: Time zone setting" .
"\n * " .
"\n */" .
"\n\$config->timezone = '$values[timezone]';" .
"\n\$config->timezone = '$values[timezone]';" .
"\n" .
"\n/**" .
"\n * Installer: Admin theme" .
"\n * " .
"\n */" .
"\n\$config->defaultAdminTheme = 'AdminThemeUikit';" .
"\n" .
"\n/**" .
"\n * Installer: Unix timestamp of date/time installed" .
@@ -761,10 +819,10 @@ class Installer {
if(($fp = fopen("./site/config.php", "a")) && fwrite($fp, $cfg)) {
fclose($fp);
$this->ok("Saved configuration to ./site/config.php");
$this->alertOk("Saved configuration to ./site/config.php");
return true;
} else {
$this->err("Error saving configuration to ./site/config.php. Please make sure it is writable.");
$this->alertErr("Error saving configuration to ./site/config.php. Please make sure it is writable.");
return false;
}
}
@@ -779,13 +837,15 @@ class Installer {
protected function profileImport($database, array $options) {
if(self::TEST_MODE) {
$this->ok("TEST MODE: Skipping profile import");
$this->alertOk("TEST MODE: Skipping profile import");
$this->adminAccount();
return;
}
$profile = "./site/install/";
if(!is_file("{$profile}install.sql")) die("No installation profile found in {$profile}");
if(!is_file("{$profile}install.sql")) die("No installation profile found in {$profile}");
$this->sectionStart('fa-building-o Profile Import');
// checks to see if the database exists using an arbitrary query (could just as easily be something else)
try {
@@ -830,7 +890,7 @@ class Installer {
} else {
// they are installing site-default already
}
$this->sectionStop();
$this->adminAccount();
}
@@ -893,7 +953,7 @@ class Installer {
$replace = array();
if($options['dbEngine'] != 'MyISAM') {
$replace['ENGINE=MyISAM'] = "ENGINE=$options[dbEngine]";
$this->warn("Engine changed to '$options[dbEngine]', please keep an eye out for issues.");
// $this->alertWarn("Engine changed to '$options[dbEngine]', please keep an eye out for issues.");
}
if($options['dbCharset'] != 'utf8') {
$replace['CHARSET=utf8'] = "CHARSET=$options[dbCharset]";
@@ -905,7 +965,7 @@ class Installer {
$replace['(255)'] = '(250)'; // max ley length in utf8mb4 is 1000 (250 * 4)
}
}
$this->warn("Character set has been changed to '$options[dbCharset]', please keep an eye out for issues.");
// $this->alertWarn("Character set has been changed to '$options[dbCharset]', please keep an eye out for issues.");
}
if(count($replace)) $restoreOptions['findReplaceCreateTable'] = $replace;
require("./wire/core/WireDatabaseBackup.php");
@@ -915,7 +975,7 @@ class Installer {
$this->ok("Imported database file: $file1");
$this->ok("Imported database file: $file2");
} else {
foreach($backup->errors() as $error) $this->err($error);
foreach($backup->errors() as $error) $this->alertErr($error);
}
}
@@ -943,25 +1003,41 @@ class Installer {
$clean[$key] = $value;
}
$this->h("Admin Panel Information");
$this->sectionStart("fa-sign-in Admin Panel");
$this->input("admin_name", "Admin Login URL", $clean['admin_name'], false, "name");
/*
$js = "$('link#colors').attr('href', $('link#colors').attr('href').replace(/main-.*$/, 'main-' + $(this).val() + '.css'))";
echo "<p class='ui-helper-clearfix'><label>Color Theme<br /><select name='colors' id='colors' onchange=\"$js\">";
foreach($this->colors as $color) echo "<option value='$color'>" . ucfirst($color) . "</option>";
echo "</select></label> <span class='detail'><i class='fa fa-angle-left'></i> Change for a live preview</span></p>";
*/
$this->clear();
$this->p("<i class='fa fa-info-circle'></i> You can change the admin URL later by editing the admin page and changing the name on the settings tab.<br /><i class='fa fa-info-circle'></i> You can change the colors later by going to Admin <i class='fa fa-angle-right'></i> Modules <i class='fa fa-angle-right detail'></i> Core <i class='fa fa-angle-right detail'></i> Admin Theme <i class='fa fa-angle-right'></i> Settings.", "detail");
$this->h("Admin Account Information");
$this->p("You will use this account to login to your ProcessWire admin. It will have superuser access, so please make sure to create a <a target='_blank' href='http://en.wikipedia.org/wiki/Password_strength'>strong password</a>.");
$this->p(
"fa-info-circle You can change the admin URL later by editing the admin page and changing the name on the settings tab.",
array('class' => 'detail', 'style' => 'margin-top:0')
);
$this->sectionStop();
$this->sectionStart("fa-user-circle Admin Account");
$this->p(
"You will use this account to login to your ProcessWire admin. It will have superuser access, so please make sure " .
"to create a <a target='_blank' href='http://en.wikipedia.org/wiki/Password_strength'>strong password</a>."
);
$this->input("username", "User", $clean['username'], false, "name");
$this->input("userpass", "Password", $clean['userpass'], false, "password");
$this->input("userpass_confirm", "Password <small class='detail'>(again)</small>", $clean['userpass_confirm'], true, "password");
$this->input("userpass_confirm", "Password <small class='detail'>(again)</small>", $clean['userpass_confirm'], false, "password");
$this->input("useremail", "Email Address", $clean['useremail'], true, "email");
$this->p("<i class='fa fa-warning'></i> Please remember the password you enter above as you will not be able to retrieve it again.", "detail");
$this->p(
"fa-warning Please remember the password you enter above as you will not be able to retrieve it again.",
array('class' => 'detail', 'style' => 'margin-top:0')
);
$this->sectionStop();
$this->h("Cleanup");
$this->sectionStart("fa-bath Cleanup");
$this->p("Directories and files listed below are no longer needed and should be removed. If you choose to leave any of them in place, you should delete them before migrating to a production environment.", "detail");
$this->p($this->getRemoveableItems($wire, true));
$this->sectionStop();
$this->btn("Continue", 5);
}
@@ -1008,7 +1084,7 @@ class Installer {
$note = $disabled ? "<span class='detail'>(not writable/deletable by this installer)</span>" : "";
$markup =
"<label style='font-weight: normal;'>" .
"<input type='checkbox' $checked $disabled name='remove_items[]' value='$name' /> $item[label] $note" .
"<input class='uk-checkbox' type='checkbox' $checked $disabled name='remove_items[]' value='$name' /> $item[label] $note" .
"</label>";
$items[$name]['markup'] = $markup;
$out .= $out ? "<br />$markup" : $markup;
@@ -1048,7 +1124,8 @@ class Installer {
protected function adminAccountSave($wire) {
$input = $wire->input;
$sanitizer = $wire->sanitizer;
$sanitizer = $wire->sanitizer;
$adminTheme = $wire->modules->getInstall('AdminThemeUikit');
if(!$input->post('username') || !$input->post('userpass')) $this->err("Missing account information");
if($input->post('userpass') !== $input->post('userpass_confirm')) $this->err("Passwords do not match");
@@ -1084,6 +1161,7 @@ class Installer {
$user->name = $username;
$user->pass = $input->post('userpass');
$user->email = $email;
$user->admin_theme = $adminTheme;
if(!$user->roles->has("superuser")) $user->roles->add($superuserRole);
@@ -1107,9 +1185,10 @@ class Installer {
$adminName = htmlentities($adminName, ENT_QUOTES, "UTF-8");
$this->h("Admin Account Saved");
$this->sectionStart("fa-user-circle Admin Account Saved");
$this->ok("User account saved: <b>{$user->name}</b>");
/*
$colors = $wire->sanitizer->pageName($input->post('colors'));
if(!in_array($colors, $this->colors)) $colors = reset($this->colors);
$theme = $wire->modules->getInstall('AdminThemeDefault');
@@ -1118,20 +1197,28 @@ class Installer {
$configData['colors'] = $colors;
$wire->modules->saveModuleConfigData('AdminThemeDefault', $configData);
$this->ok("Saved admin color set <b>$colors</b> - you will see this when you login.");
*/
$this->sectionStop();
$this->h("Complete &amp; Secure Your Installation");
$this->sectionStart("fa-life-buoy Complete &amp; Secure Your Installation");
$this->getRemoveableItems($wire, false, true);
$this->ok("Note that future runtime errors are logged to <b>/site/assets/logs/errors.txt</b> (not web accessible).");
$this->ok("For more configuration options see <b>/wire/config.php</b>.");
$this->warn("Please make your <b>/site/config.php</b> file non-writable, and readable only to you and Apache.");
$this->p("<a target='_blank' href='https://processwire.com/docs/security/file-permissions/#securing-your-site-config.php-file'>How to secure your /site/config.php file <i class='fa fa-angle-right'></i></a>");
$this->ok("For more configuration options see <b>/wire/config.php</b> and place any edits in /site/config.php.");
$this->p(
"Please make your <b>/site/config.php</b> file non-writable, and readable only to you and Apache.<br />" .
"<a target='_blank' href='https://processwire.com/docs/security/file-permissions/#securing-your-site-config.php-file'>" .
"How to secure your /site/config.php file <i class='fa fa-angle-right'></i></a>"
);
$this->sectionStop();
if(is_writable("./site/modules/")) wireChmod("./site/modules/", true);
$this->h("Use The Site!");
$this->sectionStart("fa-coffee Use The Site!");
$this->ok("Your admin URL is <a href='./$adminName/'>/$adminName/</a>");
$this->p("If you'd like, you may change this later by editing the admin page and changing the name.", "detail");
$this->sectionStop();
$this->btn("Login to Admin", 1, 'sign-in', false, true, "./$adminName/");
$this->btn("View Site ", 1, 'angle-right', true, false, "./");
@@ -1146,6 +1233,44 @@ class Installer {
* OUTPUT FUNCTIONS
*
*/
/**
* @param string $str
*
*/
protected function alertOk($str) {
if($this->inSection) {
$this->ok($str);
} else {
echo "\n<div class='uk-alert uk-alert-primary'><i class='fa fa-fw fa-check'></i> $str</div>";
}
}
/**
* @param string $str
*
*/
protected function alertWarn($str) {
if($this->inSection) {
$this->warn($str);
} else {
$this->numErrors++;
echo "\n<div class='uk-alert uk-alert-warning'><i class='fa fa-fw fa-exclamation-triangle'></i> $str</div>";
}
}
/**
* @param string $str
*
*/
protected function alertErr($str) {
if($this->inSection) {
$this->err($str);
} else {
$this->numErrors++;
echo "\n<div class='uk-alert uk-alert-danger'><i class='fa fa-fw fa-exclamation-triangle'></i> $str</div>";
}
}
/**
* Report and log an error
@@ -1155,8 +1280,13 @@ class Installer {
*
*/
protected function err($str) {
$this->numErrors++;
echo "\n<li class='ui-state-error'><i class='fa fa-exclamation-triangle'></i> $str</li>";
if(!$this->inSection) {
$this->alertErr($str);
} else {
$this->numErrors++;
//echo "\n<li class='ui-state-error'><i class='fa fa-exclamation-triangle'></i> $str</li>";
echo "\n<div class='uk-text-danger'><i class='fa fa-fw fa-exclamation-triangle'></i> $str</div>";
}
return false;
}
@@ -1168,8 +1298,13 @@ class Installer {
*
*/
protected function warn($str) {
$this->numErrors++;
echo "\n<li class='ui-state-error ui-priority-secondary'><i class='fa fa-asterisk'></i> $str</li>";
if(!$this->inSection) {
$this->alertWarn($str);
} else {
$this->numErrors++;
//echo "\n<li class='ui-state-error ui-priority-secondary'><i class='fa fa-asterisk'></i> $str</li>";
echo "\n<div class='uk-text-danger'><i class='fa fa-fw fa-asterisk'></i> $str</div>";
}
return false;
}
@@ -1181,7 +1316,12 @@ class Installer {
*
*/
protected function ok($str) {
echo "\n<li class='ui-state-highlight'><i class='fa fa-check-square-o'></i> $str</li>";
if(!$this->inSection) {
$this->alertOk($str);
} else {
//echo "\n<li class='ui-state-highlight'><i class='fa fa-check-square-o'></i> $str</li>";
echo "\n<div class=''><i class='fa fa-fw fa-check'></i> $str</div>";
}
return true;
}
@@ -1198,7 +1338,7 @@ class Installer {
*/
protected function btn($label, $value, $icon = 'angle-right', $secondary = false, $float = false, $href = '') {
$class = $secondary ? 'ui-priority-secondary' : '';
if($float) $class .= " floated";
if($float) $class .= " uk-float-left";
$type = 'submit';
if($href) $type = 'button';
if($href) echo "<a href='$href'>";
@@ -1213,22 +1353,40 @@ class Installer {
* Output a headline
*
* @param string $label
* @param string $icon
*
*/
protected function h($label) {
echo "\n<h2>$label</h2>";
protected function h($label, $icon = '') {
if(strpos($label, 'fa-') === 0) {
list($icon, $label) = explode(' ', $label, 2);
}
if($icon) {
if(strpos($icon, 'fa-') !== 0) $icon = "fa-$icon";
$icon = "<i class='fa fa-fw $icon'></i> ";
}
echo "\n<h2>$icon$label</h2>";
}
/**
* Output a paragraph
*
* @param string $text
* @param string $class
* @param string|array $class Class name, or array of attributes
*
*/
protected function p($text, $class = '') {
if($class) echo "\n<p class='$class'>$text</p>";
else echo "\n<p>$text</p>";
$icon = '';
if(strpos($text, 'fa-') === 0) list($icon, $text) = explode(' ', $text, 2);
if($icon) $icon = "<i class='fa fa-fw $icon'></i> ";
if(is_array($class)) {
echo "\n<p";
foreach($class as $k => $v) echo " $k='$v'";
echo ">$icon$text</p>";
} else if($class) {
echo "\n<p class='$class'>$icon$text</p>";
} else {
echo "\n<p>$icon$text</p>";
}
}
/**
@@ -1243,7 +1401,7 @@ class Installer {
*
*/
protected function input($name, $label, $value, $clear = false, $type = "text", $required = true) {
$width = 135;
$width = 150;
$required = $required ? "required='required'" : "";
$pattern = '';
$note = '';
@@ -1254,12 +1412,91 @@ class Installer {
$type = 'text';
$pattern = "pattern='[-_a-z0-9]{2,50}' ";
if($name == 'admin_name') $width = ($width*2);
$note = "<small class='detail' style='font-weight: normal;'>(a-z 0-9)</small>";
//$note = "<small class='detail' style='font-weight: normal;'>(a-z 0-9)</small>";
$note = "<span class='uk-text-small uk-text-muted'>(a-z 0-9)</span>";
}
$inputWidth = $width - 15;
$value = htmlentities($value, ENT_QUOTES, "UTF-8");
echo "\n<p style='width: {$width}px; float: left; margin-top: 0;'><label>$label $note<br /><input type='$type' name='$name' value='$value' $required $pattern style='width: {$inputWidth}px;' /></label></p>";
if($clear) echo "\n<br style='clear: both;' />";
echo "\n<p style='width: {$width}px; float: left; margin-top: 0;'><label>$label $note<br />";
echo "<input class='uk-input' type='$type' name='$name' value='$value' $required $pattern style='width: {$inputWidth}px;' /></label></p>";
if($clear) $this->clear();
}
/**
* Output a <select>
*
* @param string $name
* @param string $label
* @param string $value
* @param array $options
* @param int $width
*
*/
protected function select($name, $label, $value, array $options, $width = 150) {
if($width) {
$inputWidth = $width - 15;
$inputStyle = " style='width: {$inputWidth}px'";
echo "\n<p style='width: {$width}px; float: left; margin-top: 0;'>";
} else {
$inputStyle = '';
echo "\n<p style='margin-top:0'>";
}
if($label) echo "<label>$label</label><br />";
echo "\n\t<select class='uk-select' name='$name'$inputStyle>";
foreach($options as $k => $v) {
if(is_int($k)) $k = $v; // make non-assoc array behave same as assoc
$selected = $k === $value ? " selected='selected'" : "";
echo "\n\t\t<option value='$k'$selected>$v</option>";
}
echo "\n\t</select>";
echo "\n</p>";
}
protected function selectTimezone($value) {
echo "\n<p style='width:240px'>";
echo "\n\t<select class='uk-select' name='timezone'>";
foreach($this->timezones() as $key => $timezone) {
$label = $timezone;
if(strpos($label, '|')) list($label, $timezone) = explode('|', $label);
$selected = $timezone == $value ? "selected='selected'" : '';
$label = str_replace('_', ' ', $label);
echo "\n\t\t<option value=\"$key\" $selected>$label</option>";
}
echo "\n\t</select>\n</p>";
}
protected function textarea($name, $label, $value, $rows = 0) {
$rows = $rows ? " rows='$rows'" : "";
$value = htmlentities($value, ENT_QUOTES, 'UTF-8');
echo "\n<p>";
if($label) echo "\n\t<label for='textarea_$name'>$label</label><br />";
echo "\n\t<textarea class='uk-textarea' id='textarea_$name' name='$name'$rows style='width: 100%;'>$value</textarea>";
echo "\n</p>";
}
protected function sectionStart($headline = '', $type = 'muted') {
echo "\n<div class='uk-section uk-section-small uk-section-$type uk-padding uk-margin'>";
echo "\n\t<div class='uk-container'>";
$icon = '';
if(strpos($headline, 'fa-') === 0) {
list($icon, $headline) = explode(' ', $headline, 2);
$icon = "<i class='fa fa-fw $icon'></i> ";
}
if($headline) echo "<h2>$icon$headline</h2>";
$this->inSection = true;
}
protected function sectionStop() {
echo "\n\t</div>\n</div>";
$this->inSection = false;
}
protected function clear() {
echo "\n<div style='clear: both;'></div>";
}
@@ -1280,10 +1517,10 @@ class Installer {
if(self::TEST_MODE) return true;
if(is_dir($path) || mkdir($path)) {
chmod($path, octdec($this->chmodDir));
if($showNote) $this->ok("Created directory: $path");
if($showNote) $this->alertOk("Created directory: $path");
return true;
} else {
if($showNote) $this->err("Error creating directory: $path");
if($showNote) $this->alertErr("Error creating directory: $path");
return false;
}
}

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

@@ -0,0 +1,4 @@
<?php
// Intentionally left blank to test that htaccess rewrite rules are working.
// Accessing this file from http should produce a '403 forbidden' error,
// since all PHP files are blocked under /assets/.

44
site-regular/config.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
/**
* ProcessWire Configuration File
*
* Site-specific configuration for ProcessWire.
* This config.php file was generated by the ProcessExportProfile module.
*
* Please see the file /wire/config.php which contains all configuration options you may
* specify here. Simply copy any of the configuration options from that file and paste
* them into this file in order to modify them.
*
* ProcessWire 3.x
* Copyright (C) 2018 by Ryan Cramer
*
* https://processwire.com
*
*/
if(!defined("PROCESSWIRE")) die();
/*** SITE CONFIG *************************************************************************/
/**
* Enable debug mode?
*
* Debug mode causes additional info to appear for use during dev and debugging.
* This is almost always recommended for sites in development. However, you should
* always have this disabled for live/production sites.
*
* @var bool
*
*/
$config->debug = true;
$config->prependTemplateFile = '_init.php';
$config->appendTemplateFile = '_main.php';
$config->useMarkupRegions = true;
$config->useFunctionsAPI = true;
/*** INSTALLER CONFIG ********************************************************************/

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -0,0 +1,6 @@
<?php if(!defined("PROCESSWIRE_INSTALL")) die();
$info = array(
'title' => "Regular Uikit 3.x site/blog profile",
'summary' => "This is a simple/regular blog site profile that uses Uikit 3 on the front-end and demonstrates several features new to ProcessWire 3.x.",
'screenshot' => "screen_shot_2017-01-27_at_1_30_19_pm.png"
);

View File

@@ -0,0 +1,718 @@
# --- WireDatabaseBackup {"time":"2017-01-27 13:32:39","user":"","dbName":"pw_xyz","description":"","tables":[],"excludeTables":["pages_drafts","pages_roles","permissions","roles","roles_permissions","users","users_roles","user","role","permission"],"excludeCreateTables":[],"excludeExportTables":["field_roles","field_permissions","field_email","field_pass","caches","session_login_throttle","page_path_history"]}
DROP TABLE IF EXISTS `caches`;
CREATE TABLE `caches` (
`name` varchar(191) NOT NULL,
`data` mediumtext NOT NULL,
`expires` datetime NOT NULL,
PRIMARY KEY (`name`),
KEY `expires` (`expires`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `field_body`;
CREATE TABLE `field_body` (
`pages_id` int(10) unsigned NOT NULL,
`data` mediumtext NOT NULL,
PRIMARY KEY (`pages_id`),
FULLTEXT KEY `data` (`data`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `field_body` (`pages_id`, `data`) VALUES('1', '<p>This is a simple <a href=\"https://processwire.com\">ProcessWire</a> site profile that is somewhat like our default site profile, but also includes a blog. It demonstrates development of various features including some new to ProcessWire 3.x. The front-end of this profile uses the <a href=\"http://www.getuikit.com\" target=\"_blank\" rel=\"noreferrer\">Uikit 3</a> library and includes a library of time-saving functions for working with Uikit 3. Below are a few highlights you\'ll find in this site profile:</p>\n\n<ul><li>Use of markup regions and the new ProcessWire functions API.</li>\n <li>Use of Uikit 3 in template files and includes a handy PHP library of Uikit-specific functions.</li>\n <li>Demonstrates front-end editing features on <a data-pwid=1024 href=\"/about/front-end-editor-demo/\">this page</a>.</li>\n <li>Uses pagination (after 10+ blog posts) and demonstrates use of comments as well.</li>\n <li>Demonstrates caching of markup (see mobile off-canvas navigation).</li>\n <li>Demonstrates use of a Page reference field, as used by categories in the blog.</li>\n <li>The template files are easy-to-read and modify, and serve as a good platform to build from.</li>\n <li>Demonstrates implementation of a custom hook function (see in the /site/ready.php file).</li>\n</ul>');
INSERT INTO `field_body` (`pages_id`, `data`) VALUES('27', '<h3>The page you were looking for is not found.</h3>\n\n<p>Please use the navigation above to find the page, or use the search engine in the footer. </p>');
INSERT INTO `field_body` (`pages_id`, `data`) VALUES('1001', '<p>Dolore ad nunc, mos accumsan paratus duis suscipit luptatum facilisis macto uxor iaceo quadrum. Demoveo, appellatio elit neque ad commodo ea. Wisi, iaceo, tincidunt at commoveo rusticus et, ludus. Feugait at blandit bene blandit suscipere abdo duis ideo bis commoveo pagus ex, velit. Consequat commodo roto accumsan, duis transverbero.</p>');
INSERT INTO `field_body` (`pages_id`, `data`) VALUES('1002', '<p>Iusto incassum appellatio cui macto genitus vel. Lobortis aliquam luctus, roto enim, imputo wisi tamen. Ratis odio, genitus acsi, neo illum consequat consectetuer ut.</p>\n\n<p>Patria iriure vel vel autem proprius indoles ille sit. Tation blandit refoveo, accumsan ut ulciscor lucidus inhibeo capto aptent opes, foras.</p>\n\n<h3>Dolore ea valde refero feugait utinam luctus</h3>\n\n<p><img alt=\"Copyright by Austin Cramer for DesignIntelligence. This is a placeholder while he makes new ones for us.\" class=\"align_right hidpi\" src=\"/site/assets/files/1002/psych_cartoon_4-20.300x0-is-hidpi.jpg\" width=\"300\" />Usitas, nostrud transverbero, in, amet, nostrud ad. Ex feugiat opto diam os aliquam regula lobortis dolore ut ut quadrum. Esse eu quis nunc jugis iriure volutpat wisi, fere blandit inhibeo melior, hendrerit, saluto velit. Eu bene ideo dignissim delenit accumsan nunc. Usitas ille autem camur consequat typicus feugait elit ex accumsan nutus accumsan nimis pagus, occuro. Immitto populus, qui feugiat opto pneum letalis paratus. Mara conventio torqueo nibh caecus abigo sit eum brevitas. Populus, duis ex quae exerci hendrerit, si antehabeo nobis, consequat ea praemitto zelus.</p>\n\n<p>Immitto os ratis euismod conventio erat jus caecus sudo. code test Appellatio consequat, et ibidem ludus nulla dolor augue abdo tego euismod plaga lenis. Sit at nimis venio venio tego os et pecus enim pneum magna nobis ad pneum. Saepius turpis probo refero molior nonummy aliquam neque appellatio jus luctus acsi. Ulciscor refero pagus imputo eu refoveo valetudo duis dolore usitas. Consequat suscipere quod torqueo ratis ullamcorper, dolore lenis, letalis quia quadrum plaga minim.</p>');
INSERT INTO `field_body` (`pages_id`, `data`) VALUES('1004', '<p>Magna in gemino, gilvus iusto capto jugis abdo mos aptent acsi qui. Utrum inhibeo humo humo duis quae. Lucidus paulatim facilisi scisco quibus hendrerit conventio adsum.</p>\n\n<h2>Si lobortis singularis genitus ibidem saluto</h2>\n\n<ul><li>Feugiat eligo foras ex elit sed indoles hos elit ex antehabeo defui et nostrud.</li>\n <li>Letatio valetudo multo consequat inhibeo ille dignissim pagus et in quadrum eum eu.</li>\n <li>Aliquam si consequat, ut nulla amet et turpis exerci, adsum luctus ne decet, delenit.</li>\n <li>Commoveo nunc diam valetudo cui, aptent commoveo at obruo uxor nulla aliquip augue.</li>\n</ul><p>Iriure, ex velit, praesent vulpes delenit capio vero gilvus inhibeo letatio aliquip metuo qui eros. Transverbero demoveo euismod letatio torqueo melior. Ut odio in suscipit paulatim amet huic letalis suscipere eros causa, letalis magna.</p>\n\n<ol><li>Feugiat eligo foras ex elit sed indoles hos elit ex antehabeo defui et nostrud.</li>\n <li>Letatio valetudo multo consequat inhibeo ille dignissim pagus et in quadrum eum eu.</li>\n <li>Aliquam si consequat, ut nulla amet et turpis exerci, adsum luctus ne decet, delenit.</li>\n <li>Commoveo nunc diam valetudo cui, aptent commoveo at obruo uxor nulla aliquip augue.</li>\n</ol>');
INSERT INTO `field_body` (`pages_id`, `data`) VALUES('1015', '<p>Fixed effect pulse current remote integer potentiometer anomoly. Gigabyte recognition deviation active sequential bypass echo distributed. Embedded encapsulated mainframe reducer logarithmic potentiometer duplex. Software metafile reducer deviation boolean overflow bridgeware.</p>\n\n<p>Patch internet nano. Converter a inversion recursive adaptive encapsulated transport floating-point transistorized plasma microscopic node. PC duplex partitioned. Network scalar dithering encapsulated generator normalizing. Remote interval fixed plasma normalizing microscopic procedural scalar dynamic read-only high boolean.</p>\n\n<h3>Reducer hybrid force key</h3>\n\n<p>Cascading wave network logarithmic digital powered scan. Frequency coordinated particle transmission supporting. Log distributed bus scan force particle computer inversion servicing reverberated device. In coordinated services backbone silicon hyperlinked. Scalar error fiber transponder digital.</p>\n\n<p>Vector developer connectivity connectivity modular supporting broadband solution. For modular vector timer indeterminate debugged optical kilohertz procedural procedural. Infrared fuzzy procedural capacitance fiber. Algorithm direct procedural echo. Digital bridgeware by timer fragmentation ethernet inducer phase network.</p>\n\n<p>Transaction active by. Effect partitioned by timer system services computer. Spawned coordinated developer fuzzy. Technician fuzzy supporting protocol coordinated ethernet. Bridgeware video remote prototype development.</p>');
INSERT INTO `field_body` (`pages_id`, `data`) VALUES('1021', '<p>Grown plus industry open for when when sharpest ordinary offer by. Better huggable opportunity too. Rosy sleek while exclusive gentle not on. Offer colossal silky this sweet magically announcing durable sold soaking our try. Sold one zesty velvety awesome flavored ever with effervescent gentle. Screamin\' improved permanent treat now tasty we space 100%.</p>\n\n<p>Think affordable artificial blast while choice. Appetizing available really thank-you out proven desire fresh rich. Natural and flash power effective grand premium. Secret lifetime grand quenches by ocean as comfort golden youthful fast. Disposable zesty dazzling open sure spacious multi-purpose the super market rare.</p>\n\n<p>Spring special bigger wherever only this comfort tummy extravaganza save. Very messy keen leading incredible.</p>\n\n<p>Hearty brand chocolatey comfort admire ultra. Want kids touch discount love appetizing talking inside buttery. For keeps admire youthful. Wherever super thirsty lasting limited discover picky can\'t.</p>\n\n<p>Good appreciate flexible product best. Full-bodied don\'t customer gigantic also.</p>');
INSERT INTO `field_body` (`pages_id`, `data`) VALUES('1022', '<p>Genuine symphony solid educated de-jour regal gifted guests. Using gilded member silk dignified gilded panoramic art politically. Diamond upper brokerage pleasure society reserved. First-class topiary treasure travel is the best wishlist vacation solid penthouse world.</p>\n\n<p>Board marquis estate career blissfull treasure saphire. Delegate cultered regal marquis cigar sterling penthouse.</p>\n\n<p>Sterling butler solid penthouse gilded gilded pedigree wine using investments cigar. Cultered doctoral symphony extra accredited. Private benefactor monogram high-rise a.</p>\n\n<p>Career gilded extra aristocratic cruise brilliant impresario. European ambassador acumen ambassador. Rare suite cruise club crafted butler grande.</p>\n\n<p>Distinctly rich auction penthouse travel.</p>');
INSERT INTO `field_body` (`pages_id`, `data`) VALUES('1024', '<p>If you are logged in with edit access to this page, you can double-click this body copy to edit it. You can also do the same to edit the headline above, or the sidebar text to the right.</p>\n\n<p>Illum aliquip loquor. Hendrerit interdico dolor zelus diam metuo causa lobortis scisco. Euismod damnum quibus ideo patria opto. Haero odio jus virtus haero pagus erat cogo diam minim vulputate autem.</p>\n\n<h3>Ullamcorper venio bene</h3>\n\n<p>Amet ea oppeto nullus esse meus immitto sudo dignissim. Letalis velit utrum luptatum ullamcorper illum ad fere molior populus ut. Et augue eligo jumentum populus nonummy virtus. Valetudo odio ex opes mos delenit immitto ex. Illum tincidunt commoveo nostrud et ratis ne vulputate vereor tego.</p>\n\n<ul><li>Capto elit vel eu esse quia</li>\n <li>Te gemino natu et augue ad</li>\n <li>Amet aliquip valde blandit olim facilisi</li>\n</ul><p>Nulla iusto pertineo camur similis enim abigo luptatum ymo nullus. Inhibeo nutus pagus capto dolus capio pecus. Pala vereor esse melior nisl bis. Veniam eros consequat.</p>');
DROP TABLE IF EXISTS `field_categories`;
CREATE TABLE `field_categories` (
`pages_id` int(10) unsigned NOT NULL,
`data` int(11) NOT NULL,
`sort` int(10) unsigned NOT NULL,
PRIMARY KEY (`pages_id`,`sort`),
KEY `data` (`data`,`pages_id`,`sort`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `field_categories` (`pages_id`, `data`, `sort`) VALUES('1015', '1017', '0');
INSERT INTO `field_categories` (`pages_id`, `data`, `sort`) VALUES('1015', '1018', '1');
INSERT INTO `field_categories` (`pages_id`, `data`, `sort`) VALUES('1021', '1018', '1');
INSERT INTO `field_categories` (`pages_id`, `data`, `sort`) VALUES('1021', '1019', '0');
INSERT INTO `field_categories` (`pages_id`, `data`, `sort`) VALUES('1022', '1019', '0');
INSERT INTO `field_categories` (`pages_id`, `data`, `sort`) VALUES('1022', '1020', '1');
INSERT INTO `field_categories` (`pages_id`, `data`, `sort`) VALUES('1021', '1027', '2');
DROP TABLE IF EXISTS `field_comments`;
CREATE TABLE `field_comments` (
`pages_id` int(10) unsigned NOT NULL,
`data` text NOT NULL,
`sort` int(10) unsigned NOT NULL,
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`status` tinyint(3) NOT NULL DEFAULT '0',
`cite` varchar(128) NOT NULL DEFAULT '',
`email` varchar(250) NOT NULL DEFAULT '',
`created` int(10) unsigned NOT NULL,
`created_users_id` int(10) unsigned NOT NULL,
`ip` varchar(15) NOT NULL DEFAULT '',
`user_agent` varchar(250) NOT NULL DEFAULT '',
`website` varchar(250) NOT NULL DEFAULT '',
`parent_id` int(10) unsigned NOT NULL DEFAULT '0',
`flags` int(10) unsigned NOT NULL DEFAULT '0',
`code` varchar(128) DEFAULT NULL,
`subcode` varchar(40) DEFAULT NULL,
`upvotes` int(10) unsigned NOT NULL DEFAULT '0',
`downvotes` int(10) unsigned NOT NULL DEFAULT '0',
`stars` tinyint(3) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `pages_id_sort` (`pages_id`,`sort`),
KEY `status` (`status`,`email`(191)),
KEY `pages_id` (`pages_id`,`status`,`created`),
KEY `created` (`created`,`status`),
KEY `code` (`code`),
KEY `subcode` (`subcode`),
FULLTEXT KEY `data` (`data`)
) ENGINE=MyISAM AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
INSERT INTO `field_comments` (`pages_id`, `data`, `sort`, `id`, `status`, `cite`, `email`, `created`, `created_users_id`, `ip`, `user_agent`, `website`, `parent_id`, `flags`, `code`, `subcode`, `upvotes`, `downvotes`, `stars`) VALUES('1021', 'They good night the piper good night good queen white as snow they magical beans winding path up the hill dragon beautiful dress. So loud magic wand took fought angry lion ding-dong. Winding path fought ran away whale swallowed crystal ball poison apple took the piper sang twinkled.', '2', '1', '1', 'Jim', 'jim@processwire.com', '1485450830', '41', '0.0.0.0', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36', '', '0', '0', 'aeHvmkn88ncb4214OXegP8uUrQy6D5UZcaD9pPxT9gPFDdEOf1EqCM6UD6JUnY7Jtv9MPNjcPrJWUxKhyh89r1H6nywk1Se_GdwAoj2guU_9YYa9MEgiuJUekuk93YvE', 'JzPW6751GqTqk1Oh__k0IbNfOi_Nc6nYvPPa2wl6', '0', '0', NULL);
INSERT INTO `field_comments` (`pages_id`, `data`, `sort`, `id`, `status`, `cite`, `email`, `created`, `created_users_id`, `ip`, `user_agent`, `website`, `parent_id`, `flags`, `code`, `subcode`, `upvotes`, `downvotes`, `stars`) VALUES('1021', 'LED harmonic nominal femtosecond data solid alphanumeric alphanumeric. By sampling bus recursive null. Modular timer recognition passive interval. Theory capacitance application fragmentation with supporting indeterminate. Microscopic record indeterminate scalar concept deviation system.', '3', '2', '1', 'ryan', 'ryan@processwire.com', '1485453231', '41', '0.0.0.0', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36', 'https://processwire.com', '0', '0', 'eWwAZPZHyC4JcShlKDPrr5Y_rC8sntJOildm2ecqUegXPwgfwmRhyOn5ssyQhABWwaweM74e_TApOLMQu4MGt9lSf7VcxH994ciwggF0f3lpEdJ3OMtjYe4MvW4gDzNF', 'CcaWnsxrcWYxfCgnnvYc2DEN7qU3bd_7HVm3NK_0', '0', '0', NULL);
INSERT INTO `field_comments` (`pages_id`, `data`, `sort`, `id`, `status`, `cite`, `email`, `created`, `created_users_id`, `ip`, `user_agent`, `website`, `parent_id`, `flags`, `code`, `subcode`, `upvotes`, `downvotes`, `stars`) VALUES('1022', 'Run Spot play help I am hungry I can help too oh no. He wants to play oh no for a ride I can help for a ride too on our bikes chase the cat for a ride. Bring it here walk we have two bark I am hungry jump high now he is funny it is Sally for a ride oh please. We can oh please down the toy I can help no Jane is looking she is happy share with them.\n\nLook too eating cake he wants to play he wants to play down don\'t worry on our bikes on our bikes. I am hungry down jump high they are silly and oh no help but. Thank you over there I can help Dick said over there on our bikes see Puff over there do it Jane is looking I can see he is fast. Quick in the wagon no jump high.', '2', '3', '1', 'Ryan', 'ryan@processwire.com', '1485528109', '41', '0.0.0.0', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36', 'https://processwire.com', '0', '0', 'CVaOB64FqwwVlmAXUbhP5KbZQDFM7OarKON1ZIb_PYP9cDkpcb0NCYp56iAPHTZoIjJEZNQ4mnuEyLkYw97XkfOgmtRDd33rfZB0Zt1yfDKOjY4tdkWP08BKUbI_MImr', 'ySOMziIfOxoi_BfPVBNxNaGVrKHGzHGtFbnbkodo', '0', '0', NULL);
DROP TABLE IF EXISTS `field_comments_votes`;
CREATE TABLE `field_comments_votes` (
`comment_id` int(10) unsigned NOT NULL,
`vote` tinyint(4) NOT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`ip` varchar(15) NOT NULL DEFAULT '',
`user_id` int(10) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`comment_id`,`ip`,`vote`),
KEY `created` (`created`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `field_date`;
CREATE TABLE `field_date` (
`pages_id` int(10) unsigned NOT NULL,
`data` datetime NOT NULL,
PRIMARY KEY (`pages_id`),
KEY `data` (`data`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `field_date` (`pages_id`, `data`) VALUES('1015', '2017-01-25 00:00:00');
INSERT INTO `field_date` (`pages_id`, `data`) VALUES('1022', '2017-01-26 00:00:00');
INSERT INTO `field_date` (`pages_id`, `data`) VALUES('1021', '2017-01-27 00:00:00');
DROP TABLE IF EXISTS `field_email`;
CREATE TABLE `field_email` (
`pages_id` int(10) unsigned NOT NULL,
`data` varchar(191) NOT NULL DEFAULT '',
PRIMARY KEY (`pages_id`),
KEY `data_exact` (`data`),
FULLTEXT KEY `data` (`data`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `field_headline`;
CREATE TABLE `field_headline` (
`pages_id` int(10) unsigned NOT NULL,
`data` text NOT NULL,
PRIMARY KEY (`pages_id`),
FULLTEXT KEY `data` (`data`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `field_headline` (`pages_id`, `data`) VALUES('1', 'Uikit 3 site/blog profile');
INSERT INTO `field_headline` (`pages_id`, `data`) VALUES('27', '404 Page Not Found');
INSERT INTO `field_headline` (`pages_id`, `data`) VALUES('1001', 'About Us');
INSERT INTO `field_headline` (`pages_id`, `data`) VALUES('1024', 'Demonstration of front-end-editing');
DROP TABLE IF EXISTS `field_images`;
CREATE TABLE `field_images` (
`pages_id` int(10) unsigned NOT NULL,
`data` varchar(191) NOT NULL,
`sort` int(10) unsigned NOT NULL,
`description` text NOT NULL,
`modified` datetime DEFAULT NULL,
`created` datetime DEFAULT NULL,
PRIMARY KEY (`pages_id`,`sort`),
KEY `data` (`data`),
KEY `modified` (`modified`),
KEY `created` (`created`),
FULLTEXT KEY `description` (`description`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `field_images` (`pages_id`, `data`, `sort`, `description`, `modified`, `created`) VALUES('1002', 'psych_cartoon_4-20.jpg', '0', 'Copyright by Austin Cramer for DesignIntelligence. This is a placeholder while he makes new ones for us.', '2017-01-24 06:11:43', '2017-01-24 06:11:43');
INSERT INTO `field_images` (`pages_id`, `data`, `sort`, `description`, `modified`, `created`) VALUES('1021', 'screen_shot_2017-01-27_at_10_46_35_am.png', '0', '', '2017-01-27 10:56:13', '2017-01-27 10:56:13');
DROP TABLE IF EXISTS `field_pass`;
CREATE TABLE `field_pass` (
`pages_id` int(10) unsigned NOT NULL,
`data` char(40) NOT NULL,
`salt` char(32) NOT NULL,
PRIMARY KEY (`pages_id`),
KEY `data` (`data`)
) ENGINE=MyISAM DEFAULT CHARSET=ascii;
DROP TABLE IF EXISTS `field_permissions`;
CREATE TABLE `field_permissions` (
`pages_id` int(10) unsigned NOT NULL,
`data` int(11) NOT NULL,
`sort` int(10) unsigned NOT NULL,
PRIMARY KEY (`pages_id`,`sort`),
KEY `data` (`data`,`pages_id`,`sort`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `field_process`;
CREATE TABLE `field_process` (
`pages_id` int(11) NOT NULL DEFAULT '0',
`data` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`pages_id`),
KEY `data` (`data`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('10', '7');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('23', '10');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('3', '12');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('8', '12');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('9', '14');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('6', '17');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('11', '47');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('16', '48');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('21', '50');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('29', '66');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('30', '68');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('22', '76');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('28', '76');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('2', '87');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('300', '104');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('301', '109');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('302', '121');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('303', '129');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('31', '136');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('304', '138');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('1007', '150');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('1009', '158');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('1011', '159');
INSERT INTO `field_process` (`pages_id`, `data`) VALUES('1025', '165');
DROP TABLE IF EXISTS `field_roles`;
CREATE TABLE `field_roles` (
`pages_id` int(10) unsigned NOT NULL,
`data` int(11) NOT NULL,
`sort` int(10) unsigned NOT NULL,
PRIMARY KEY (`pages_id`,`sort`),
KEY `data` (`data`,`pages_id`,`sort`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `field_sidebar`;
CREATE TABLE `field_sidebar` (
`pages_id` int(10) unsigned NOT NULL,
`data` mediumtext NOT NULL,
PRIMARY KEY (`pages_id`),
FULLTEXT KEY `data` (`data`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `field_sidebar` (`pages_id`, `data`) VALUES('1', '<h3>Requirements</h3>\n\n<p>This site profile requires ProcessWire 3.0.105 or newer, Uikit 3, and the server must be running on PHP 5.4 or newer.</p>');
INSERT INTO `field_sidebar` (`pages_id`, `data`) VALUES('1002', '<h3>Sudo nullus</h3>\r\n\r\n<p>Et torqueo vulpes vereor luctus augue quod consectetuer antehabeo causa patria tation ex plaga ut. Abluo delenit wisi iriure eros feugiat probo nisl aliquip nisl, patria. Antehabeo esse camur nisl modo utinam. Sudo nullus ventosus ibidem facilisis saepius eum sino pneum, vicis odio voco opto.</p>');
INSERT INTO `field_sidebar` (`pages_id`, `data`) VALUES('1024', '<h3>Double click me</h3>\n\n<p>Esca demoveo exputo sagaciter ullamcorper inhibeo ut nimis refoveo praemitto defui ut. Hendrerit ratis dignissim ea eligo. Genitus utinam suscipere caecus ad neque verto at regula saluto esse turpis. Refero autem et nulla ibidem caecus fere acsi plaga in turpis. Nobis sit nunc esse capio suscipit vulpes facilisis brevitas. Pagus odio eros accumsan et interdico nunc abdo eligo epulae.</p>');
DROP TABLE IF EXISTS `field_summary`;
CREATE TABLE `field_summary` (
`pages_id` int(10) unsigned NOT NULL,
`data` mediumtext NOT NULL,
PRIMARY KEY (`pages_id`),
FULLTEXT KEY `data` (`data`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `field_summary` (`pages_id`, `data`) VALUES('1', 'A simple blog site about nothing in particular.');
INSERT INTO `field_summary` (`pages_id`, `data`) VALUES('1001', 'This is a placeholder page with two child pages to serve as an example. ');
INSERT INTO `field_summary` (`pages_id`, `data`) VALUES('1002', 'Dolore ea valde refero feugait utinam luctus. Probo velit commoveo et, delenit praesent, suscipit zelus, hendrerit zelus illum facilisi, regula. ');
INSERT INTO `field_summary` (`pages_id`, `data`) VALUES('1004', 'Mos erat reprobo in praesent, mara premo, obruo iustum pecus velit lobortis te sagaciter populus.');
INSERT INTO `field_summary` (`pages_id`, `data`) VALUES('1005', 'View this template\'s source for a demonstration of how to create a basic site map. ');
INSERT INTO `field_summary` (`pages_id`, `data`) VALUES('1024', 'If you are logged in with edit access, pages using the basic-page-edit template (like this one) are editable on the front-end.');
DROP TABLE IF EXISTS `field_title`;
CREATE TABLE `field_title` (
`pages_id` int(10) unsigned NOT NULL,
`data` text NOT NULL,
PRIMARY KEY (`pages_id`),
KEY `data_exact` (`data`(191)),
FULLTEXT KEY `data` (`data`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1', 'Home');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('2', 'Admin');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('3', 'Pages');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('6', 'Add Page');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('7', 'Trash');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('8', 'Tree');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('9', 'Save Sort');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('10', 'Edit');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('11', 'Templates');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('16', 'Fields');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('21', 'Modules');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('22', 'Setup');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('23', 'Login');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('27', '404 Page');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('28', 'Access');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('29', 'Users');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('30', 'Roles');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('31', 'Permissions');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('32', 'Edit pages');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('34', 'Delete pages');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('35', 'Move pages (change parent)');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('36', 'View pages');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('50', 'Sort child pages');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('51', 'Change templates on pages');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('52', 'Administer users');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('53', 'User can update profile/password');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('54', 'Lock or unlock a page');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('300', 'Search');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('301', 'Empty Trash');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('302', 'Insert Link');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('303', 'Insert Image');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('304', 'Profile');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1000', 'Search');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1001', 'About');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1002', 'Child page example 1');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1004', 'Child page example 2');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1005', 'Site Map');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1006', 'Use Page Lister');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1007', 'Find');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1009', 'Recent');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1010', 'Can see recently edited pages');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1011', 'Logs');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1012', 'Can view system logs');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1013', 'Can manage system logs');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1014', 'Blog');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1015', 'Phase data extended transaction');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1016', 'Categories');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1017', 'Coffee');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1018', 'Beer');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1019', 'Plants');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1020', 'Cats');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1021', 'Think affordable artificial blast');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1022', 'Genuine symphony');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1023', 'Use the front-end page editor');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1024', 'Front-end editing demo');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1025', 'Comments');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1026', 'Use the comments manager');
INSERT INTO `field_title` (`pages_id`, `data`) VALUES('1027', 'Recipes');
DROP TABLE IF EXISTS `fieldgroups`;
CREATE TABLE `fieldgroups` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(191) CHARACTER SET ascii NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=MyISAM AUTO_INCREMENT=102 DEFAULT CHARSET=utf8;
INSERT INTO `fieldgroups` (`id`, `name`) VALUES('2', 'admin');
INSERT INTO `fieldgroups` (`id`, `name`) VALUES('83', 'basic-page');
INSERT INTO `fieldgroups` (`id`, `name`) VALUES('101', 'basic-page-edit');
INSERT INTO `fieldgroups` (`id`, `name`) VALUES('98', 'blog');
INSERT INTO `fieldgroups` (`id`, `name`) VALUES('97', 'blog-post');
INSERT INTO `fieldgroups` (`id`, `name`) VALUES('99', 'categories');
INSERT INTO `fieldgroups` (`id`, `name`) VALUES('100', 'category');
INSERT INTO `fieldgroups` (`id`, `name`) VALUES('1', 'home');
INSERT INTO `fieldgroups` (`id`, `name`) VALUES('5', 'permission');
INSERT INTO `fieldgroups` (`id`, `name`) VALUES('4', 'role');
INSERT INTO `fieldgroups` (`id`, `name`) VALUES('80', 'search');
INSERT INTO `fieldgroups` (`id`, `name`) VALUES('88', 'sitemap');
INSERT INTO `fieldgroups` (`id`, `name`) VALUES('3', 'user');
DROP TABLE IF EXISTS `fieldgroups_fields`;
CREATE TABLE `fieldgroups_fields` (
`fieldgroups_id` int(10) unsigned NOT NULL DEFAULT '0',
`fields_id` int(10) unsigned NOT NULL DEFAULT '0',
`sort` int(11) unsigned NOT NULL DEFAULT '0',
`data` text,
PRIMARY KEY (`fieldgroups_id`,`fields_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('1', '1', '0', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('1', '44', '5', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('1', '76', '3', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('1', '78', '1', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('1', '79', '2', '{\"label\":\"Site tagline\"}');
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('1', '82', '4', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('2', '1', '0', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('2', '2', '1', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('3', '3', '0', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('3', '4', '2', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('3', '92', '1', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('4', '5', '0', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('5', '1', '0', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('80', '1', '0', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('83', '1', '0', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('83', '44', '5', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('83', '76', '3', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('83', '78', '1', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('83', '79', '2', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('83', '82', '4', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('88', '1', '0', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('88', '79', '1', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('97', '1', '0', '{\"columnWidth\":75}');
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('97', '44', '3', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('97', '76', '2', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('97', '97', '1', '{\"columnWidth\":25}');
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('97', '98', '4', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('97', '99', '5', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('98', '1', '0', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('99', '1', '0', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('100', '1', '0', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('101', '1', '0', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('101', '44', '5', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('101', '76', '3', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('101', '78', '1', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('101', '79', '2', NULL);
INSERT INTO `fieldgroups_fields` (`fieldgroups_id`, `fields_id`, `sort`, `data`) VALUES('101', '82', '4', NULL);
DROP TABLE IF EXISTS `fields`;
CREATE TABLE `fields` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`type` varchar(128) CHARACTER SET ascii NOT NULL,
`name` varchar(191) CHARACTER SET ascii NOT NULL,
`flags` int(11) NOT NULL DEFAULT '0',
`label` varchar(191) NOT NULL DEFAULT '',
`data` text NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`),
KEY `type` (`type`)
) ENGINE=MyISAM AUTO_INCREMENT=100 DEFAULT CHARSET=utf8;
INSERT INTO `fields` (`id`, `type`, `name`, `flags`, `label`, `data`) VALUES('1', 'FieldtypePageTitle', 'title', '13', 'Title', '{\"required\":1,\"textformatters\":[\"TextformatterEntities\"],\"size\":0,\"maxlength\":255}');
INSERT INTO `fields` (`id`, `type`, `name`, `flags`, `label`, `data`) VALUES('2', 'FieldtypeModule', 'process', '25', 'Process', '{\"description\":\"The process that is executed on this page. Since this is mostly used by ProcessWire internally, it is recommended that you don\'t change the value of this unless adding your own pages in the admin.\",\"collapsed\":1,\"required\":1,\"moduleTypes\":[\"Process\"],\"permanent\":1}');
INSERT INTO `fields` (`id`, `type`, `name`, `flags`, `label`, `data`) VALUES('3', 'FieldtypePassword', 'pass', '24', 'Set Password', '{\"collapsed\":1,\"size\":50,\"maxlength\":128}');
INSERT INTO `fields` (`id`, `type`, `name`, `flags`, `label`, `data`) VALUES('4', 'FieldtypePage', 'roles', '24', 'Roles', '{\"derefAsPage\":0,\"parent_id\":30,\"labelFieldName\":\"name\",\"inputfield\":\"InputfieldCheckboxes\",\"description\":\"User will inherit the permissions assigned to each role. You may assign multiple roles to a user. When accessing a page, the user will only inherit permissions from the roles that are also assigned to the page\'s template.\"}');
INSERT INTO `fields` (`id`, `type`, `name`, `flags`, `label`, `data`) VALUES('5', 'FieldtypePage', 'permissions', '24', 'Permissions', '{\"derefAsPage\":0,\"parent_id\":31,\"labelFieldName\":\"title\",\"inputfield\":\"InputfieldCheckboxes\"}');
INSERT INTO `fields` (`id`, `type`, `name`, `flags`, `label`, `data`) VALUES('44', 'FieldtypeImage', 'images', '0', 'Images', '{\"extensions\":\"gif jpg jpeg png\",\"adminThumbs\":1,\"inputfieldClass\":\"InputfieldImage\",\"maxFiles\":0,\"descriptionRows\":1,\"fileSchema\":2,\"textformatters\":[\"TextformatterEntities\"],\"outputFormat\":1,\"defaultValuePage\":0,\"defaultGrid\":0,\"icon\":\"camera\"}');
INSERT INTO `fields` (`id`, `type`, `name`, `flags`, `label`, `data`) VALUES('76', 'FieldtypeTextarea', 'body', '0', 'Body', '{\"inputfieldClass\":\"InputfieldCKEditor\",\"rows\":10,\"contentType\":1,\"toolbar\":\"Format, Bold, Italic, -, RemoveFormat\\nNumberedList, BulletedList, -, Blockquote\\nPWLink, Unlink, Anchor\\nPWImage, Table, HorizontalRule, SpecialChar\\nPasteText, PasteFromWord\\nScayt, -, Sourcedialog\",\"inlineMode\":0,\"useACF\":1,\"usePurifier\":1,\"formatTags\":\"p;h2;h3;h4;h5;h6;pre;address\",\"extraPlugins\":[\"pwimage\",\"pwlink\",\"sourcedialog\"],\"removePlugins\":\"image,magicline\",\"toggles\":[2,4,8],\"htmlOptions\":[2],\"collapsed\":0,\"minlength\":0,\"maxlength\":0,\"showCount\":0}');
INSERT INTO `fields` (`id`, `type`, `name`, `flags`, `label`, `data`) VALUES('78', 'FieldtypeText', 'headline', '0', 'Headline', '{\"description\":\"Use this instead of the Title if a longer headline is needed than what you want to appear in navigation.\",\"textformatters\":[\"TextformatterEntities\"],\"collapsed\":2,\"size\":0,\"maxlength\":1024,\"minlength\":0,\"showCount\":0}');
INSERT INTO `fields` (`id`, `type`, `name`, `flags`, `label`, `data`) VALUES('79', 'FieldtypeTextarea', 'summary', '1', 'Summary', '{\"textformatters\":[\"TextformatterEntities\"],\"inputfieldClass\":\"InputfieldTextarea\",\"collapsed\":2,\"rows\":3,\"contentType\":0}');
INSERT INTO `fields` (`id`, `type`, `name`, `flags`, `label`, `data`) VALUES('82', 'FieldtypeTextarea', 'sidebar', '0', 'Sidebar', '{\"inputfieldClass\":\"InputfieldCKEditor\",\"rows\":5,\"contentType\":1,\"toolbar\":\"Format, Bold, Italic, -, RemoveFormat\\r\\nNumberedList, BulletedList, -, Blockquote\\r\\nPWLink, Unlink, Anchor\\r\\nPWImage, Table, HorizontalRule, SpecialChar\\r\\nPasteText, PasteFromWord\\r\\nScayt, -, Sourcedialog\",\"inlineMode\":0,\"useACF\":1,\"usePurifier\":1,\"formatTags\":\"p;h2;h3;h4;h5;h6;pre;address\",\"extraPlugins\":[\"pwimage\",\"pwlink\",\"sourcedialog\"],\"removePlugins\":\"image,magicline\",\"toggles\":[2,4,8],\"collapsed\":2}');
INSERT INTO `fields` (`id`, `type`, `name`, `flags`, `label`, `data`) VALUES('92', 'FieldtypeEmail', 'email', '9', 'E-Mail Address', '{\"size\":70,\"maxlength\":255}');
INSERT INTO `fields` (`id`, `type`, `name`, `flags`, `label`, `data`) VALUES('97', 'FieldtypeDatetime', 'date', '0', 'Date', '{\"dateOutputFormat\":\"j F Y\",\"collapsed\":0,\"size\":25,\"datepicker\":3,\"timeInputSelect\":0,\"dateInputFormat\":\"Y\\/m\\/d\",\"defaultToday\":1,\"placeholder\":\"yyyy\\/mm\\/dd\",\"icon\":\"calendar\"}');
INSERT INTO `fields` (`id`, `type`, `name`, `flags`, `label`, `data`) VALUES('98', 'FieldtypePage', 'categories', '0', 'Categories', '{\"derefAsPage\":0,\"inputfield\":\"InputfieldAsmSelect\",\"parent_id\":1016,\"template_id\":46,\"labelFieldName\":\"title\",\"addable\":1,\"collapsed\":0}');
INSERT INTO `fields` (`id`, `type`, `name`, `flags`, `label`, `data`) VALUES('99', 'FieldtypeComments', 'comments', '0', 'Comments', '{\"schemaVersion\":6,\"moderate\":1,\"redirectAfterPost\":1,\"quietSave\":1,\"useNotify\":0,\"deleteSpamDays\":3,\"depth\":0,\"useWebsite\":1,\"dateFormat\":\"relative\",\"useVotes\":0,\"useStars\":0,\"useGravatar\":\"g\",\"collapsed\":0}');
DROP TABLE IF EXISTS `modules`;
CREATE TABLE `modules` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`class` varchar(128) CHARACTER SET ascii NOT NULL,
`flags` int(11) NOT NULL DEFAULT '0',
`data` text NOT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `class` (`class`)
) ENGINE=MyISAM AUTO_INCREMENT=167 DEFAULT CHARSET=utf8;
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('1', 'FieldtypeTextarea', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('2', 'FieldtypeNumber', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('3', 'FieldtypeText', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('4', 'FieldtypePage', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('6', 'FieldtypeFile', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('7', 'ProcessPageEdit', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('10', 'ProcessLogin', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('12', 'ProcessPageList', '0', '{\"pageLabelField\":\"title\",\"paginationLimit\":25,\"limit\":50}', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('14', 'ProcessPageSort', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('15', 'InputfieldPageListSelect', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('17', 'ProcessPageAdd', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('25', 'InputfieldAsmSelect', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('27', 'FieldtypeModule', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('28', 'FieldtypeDatetime', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('29', 'FieldtypeEmail', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('30', 'InputfieldForm', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('32', 'InputfieldSubmit', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('33', 'InputfieldWrapper', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('34', 'InputfieldText', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('35', 'InputfieldTextarea', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('36', 'InputfieldSelect', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('37', 'InputfieldCheckbox', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('38', 'InputfieldCheckboxes', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('39', 'InputfieldRadios', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('40', 'InputfieldHidden', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('41', 'InputfieldName', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('43', 'InputfieldSelectMultiple', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('45', 'JqueryWireTabs', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('46', 'ProcessPage', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('47', 'ProcessTemplate', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('48', 'ProcessField', '32', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('50', 'ProcessModule', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('55', 'InputfieldFile', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('56', 'InputfieldImage', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('57', 'FieldtypeImage', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('60', 'InputfieldPage', '0', '{\"inputfieldClasses\":[\"InputfieldSelect\",\"InputfieldSelectMultiple\",\"InputfieldCheckboxes\",\"InputfieldRadios\",\"InputfieldAsmSelect\",\"InputfieldPageListSelect\",\"InputfieldPageListSelectMultiple\",\"InputfieldPageAutocomplete\"]}', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('61', 'TextformatterEntities', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('66', 'ProcessUser', '0', '{\"showFields\":[\"name\",\"email\",\"roles\"]}', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('67', 'MarkupAdminDataTable', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('68', 'ProcessRole', '0', '{\"showFields\":[\"name\"]}', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('76', 'ProcessList', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('78', 'InputfieldFieldset', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('79', 'InputfieldMarkup', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('80', 'InputfieldEmail', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('83', 'ProcessPageView', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('84', 'FieldtypeInteger', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('85', 'InputfieldInteger', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('86', 'InputfieldPageName', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('87', 'ProcessHome', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('89', 'FieldtypeFloat', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('90', 'InputfieldFloat', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('94', 'InputfieldDatetime', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('97', 'FieldtypeCheckbox', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('98', 'MarkupPagerNav', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('103', 'JqueryTableSorter', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('104', 'ProcessPageSearch', '1', '{\"searchFields\":\"title\",\"displayField\":\"title path\"}', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('105', 'FieldtypeFieldsetOpen', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('106', 'FieldtypeFieldsetClose', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('107', 'FieldtypeFieldsetTabOpen', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('108', 'InputfieldURL', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('109', 'ProcessPageTrash', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('111', 'FieldtypePageTitle', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('112', 'InputfieldPageTitle', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('113', 'MarkupPageArray', '3', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('114', 'PagePermissions', '3', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('115', 'PageRender', '3', '{\"clearCache\":1}', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('116', 'JqueryCore', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('117', 'JqueryUI', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('121', 'ProcessPageEditLink', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('122', 'InputfieldPassword', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('125', 'SessionLoginThrottle', '11', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('129', 'ProcessPageEditImageSelect', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('131', 'InputfieldButton', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('133', 'FieldtypePassword', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('134', 'ProcessPageType', '33', '{\"showFields\":[]}', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('135', 'FieldtypeURL', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('136', 'ProcessPermission', '1', '{\"showFields\":[\"name\",\"title\"]}', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('137', 'InputfieldPageListSelectMultiple', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('138', 'ProcessProfile', '1', '{\"profileFields\":[\"pass\",\"email\"]}', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('139', 'SystemUpdater', '1', '{\"systemVersion\":15,\"coreVersion\":\"3.0.50\"}', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('148', 'AdminThemeDefault', '10', '{\"colors\":\"classic\"}', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('149', 'InputfieldSelector', '42', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('150', 'ProcessPageLister', '32', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('151', 'JqueryMagnific', '1', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('152', 'PagePathHistory', '3', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('155', 'InputfieldCKEditor', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('156', 'MarkupHTMLPurifier', '0', '', '2017-01-24 06:11:43');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('158', 'ProcessRecentPages', '1', '', '2017-01-24 06:12:09');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('159', 'ProcessLogger', '1', '', '2017-01-24 06:12:17');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('160', 'InputfieldIcon', '0', '', '2017-01-24 06:12:17');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('161', 'FieldtypeComments', '1', '', '2017-01-26 11:32:48');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('162', 'InputfieldCommentsAdmin', '0', '', '2017-01-26 11:32:48');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('163', 'InputfieldPageAutocomplete', '0', '', '2017-01-27 11:18:20');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('164', 'PageFrontEdit', '2', '', '2017-01-27 11:32:31');
INSERT INTO `modules` (`id`, `class`, `flags`, `data`, `created`) VALUES('165', 'ProcessCommentsManager', '1', '', '2017-01-27 12:17:47');
DROP TABLE IF EXISTS `page_path_history`;
CREATE TABLE `page_path_history` (
`path` varchar(191) NOT NULL,
`pages_id` int(10) unsigned NOT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`path`),
KEY `pages_id` (`pages_id`),
KEY `created` (`created`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `pages`;
CREATE TABLE `pages` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`parent_id` int(11) unsigned NOT NULL DEFAULT '0',
`templates_id` int(11) unsigned NOT NULL DEFAULT '0',
`name` varchar(128) CHARACTER SET ascii NOT NULL,
`status` int(10) unsigned NOT NULL DEFAULT '1',
`modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modified_users_id` int(10) unsigned NOT NULL DEFAULT '2',
`created` timestamp NOT NULL DEFAULT '2015-12-18 06:09:00',
`created_users_id` int(10) unsigned NOT NULL DEFAULT '2',
`published` datetime DEFAULT NULL,
`sort` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `name_parent_id` (`name`,`parent_id`),
KEY `parent_id` (`parent_id`),
KEY `templates_id` (`templates_id`),
KEY `modified` (`modified`),
KEY `created` (`created`),
KEY `status` (`status`),
KEY `published` (`published`)
) ENGINE=MyISAM AUTO_INCREMENT=1029 DEFAULT CHARSET=utf8;
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1', '0', '1', 'home', '9', '2017-01-27 13:29:31', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '0');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('2', '1', '2', 'processwire', '1035', '2017-01-24 06:12:10', '40', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '6');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('3', '2', '2', 'page', '21', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '0');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('6', '3', '2', 'add', '21', '2017-01-24 06:12:22', '40', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '0');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('7', '1', '2', 'trash', '1039', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '7');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('8', '3', '2', 'list', '1045', '2017-01-24 06:15:58', '40', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '1');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('9', '3', '2', 'sort', '1047', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '2');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('10', '3', '2', 'edit', '1045', '2017-01-24 06:15:58', '40', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '3');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('11', '22', '2', 'template', '21', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '0');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('16', '22', '2', 'field', '21', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '2');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('21', '2', '2', 'module', '21', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '2');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('22', '2', '2', 'setup', '21', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '1');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('23', '2', '2', 'login', '1035', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '4');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('27', '1', '29', 'http404', '1035', '2017-01-27 12:25:04', '41', '2017-01-24 06:11:43', '3', '2017-01-24 06:11:43', '5');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('28', '2', '2', 'access', '13', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '3');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('29', '28', '2', 'users', '29', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '0');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('30', '28', '2', 'roles', '29', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '1');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('31', '28', '2', 'permissions', '29', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '2');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('32', '31', '5', 'page-edit', '25', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '2');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('34', '31', '5', 'page-delete', '25', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '3');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('35', '31', '5', 'page-move', '25', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '4');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('36', '31', '5', 'page-view', '25', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '0');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('37', '30', '4', 'guest', '25', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '0');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('38', '30', '4', 'superuser', '25', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '1');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('40', '29', '3', 'guest', '25', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '1');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('41', '29', '3', 'admin', '1', '2017-01-24 06:12:10', '40', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '0');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('50', '31', '5', 'page-sort', '25', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '5');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('51', '31', '5', 'page-template', '25', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '6');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('52', '31', '5', 'user-admin', '25', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '10');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('53', '31', '5', 'profile-edit', '1', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '13');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('54', '31', '5', 'page-lock', '1', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '8');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('300', '3', '2', 'search', '1045', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '5');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('301', '3', '2', 'trash', '1047', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '5');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('302', '3', '2', 'link', '1041', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '6');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('303', '3', '2', 'image', '1041', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '7');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('304', '2', '2', 'profile', '1025', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '41', '2017-01-24 06:11:43', '5');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1000', '1', '26', 'search', '1025', '2017-01-26 09:55:14', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '4');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1001', '1', '29', 'about', '1', '2017-01-25 08:36:43', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '0');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1002', '1001', '29', 'what', '1', '2017-01-27 12:08:45', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '0');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1004', '1001', '29', 'background', '1', '2017-01-27 09:45:20', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '1');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1005', '1', '34', 'site-map', '1', '2017-01-26 09:55:10', '41', '2017-01-24 06:11:43', '2', '2017-01-24 06:11:43', '3');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1006', '31', '5', 'page-lister', '1', '2017-01-24 06:11:43', '40', '2017-01-24 06:11:43', '40', '2017-01-24 06:11:43', '9');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1007', '3', '2', 'lister', '1', '2017-01-24 06:11:43', '40', '2017-01-24 06:11:43', '40', '2017-01-24 06:11:43', '8');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1009', '3', '2', 'recent-pages', '1', '2017-01-24 06:12:09', '40', '2017-01-24 06:12:09', '40', '2017-01-24 06:12:09', '9');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1010', '31', '5', 'page-edit-recent', '1', '2017-01-24 06:12:09', '40', '2017-01-24 06:12:09', '40', '2017-01-24 06:12:09', '10');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1011', '22', '2', 'logs', '1', '2017-01-24 06:12:17', '40', '2017-01-24 06:12:17', '40', '2017-01-24 06:12:17', '2');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1012', '31', '5', 'logs-view', '1', '2017-01-24 06:12:17', '40', '2017-01-24 06:12:17', '40', '2017-01-24 06:12:17', '11');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1013', '31', '5', 'logs-edit', '1', '2017-01-24 06:12:17', '40', '2017-01-24 06:12:17', '40', '2017-01-24 06:12:17', '12');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1014', '1', '44', 'blog', '1', '2017-01-25 15:22:52', '41', '2017-01-25 15:22:52', '41', '2017-01-25 15:22:52', '1');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1015', '1014', '43', 'phase-data-extended-transaction', '1', '2017-01-26 06:18:13', '41', '2017-01-25 15:23:04', '41', '2017-01-25 15:23:20', '0');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1016', '1', '45', 'categories', '1', '2017-01-26 05:55:33', '41', '2017-01-26 05:54:06', '41', '2017-01-26 05:54:06', '2');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1017', '1016', '46', 'coffee', '1', '2017-01-26 05:54:49', '41', '2017-01-26 05:54:46', '41', '2017-01-26 05:54:46', '0');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1018', '1016', '46', 'beer', '1', '2017-01-26 05:54:53', '41', '2017-01-26 05:54:53', '41', '2017-01-26 05:54:53', '1');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1019', '1016', '46', 'plants', '1', '2017-01-26 05:56:01', '41', '2017-01-26 05:56:01', '41', '2017-01-26 05:56:01', '2');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1020', '1016', '46', 'cats', '1', '2017-01-26 06:10:41', '41', '2017-01-26 06:10:41', '41', '2017-01-26 06:10:41', '3');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1021', '1014', '43', 'think-affordable-artificial-blast', '1', '2017-01-27 12:37:31', '41', '2017-01-26 06:38:37', '41', '2017-01-26 06:39:03', '1');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1022', '1014', '43', 'genuine-symphony', '1', '2017-01-27 12:18:48', '41', '2017-01-26 09:50:20', '41', '2017-01-26 09:50:54', '2');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1023', '31', '5', 'page-edit-front', '1', '2017-01-27 11:32:31', '41', '2017-01-27 11:32:31', '41', '2017-01-27 11:32:31', '13');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1024', '1001', '47', 'front-end-editor-demo', '1', '2017-01-27 12:31:38', '41', '2017-01-27 12:01:56', '41', '2017-01-27 12:03:43', '2');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1025', '22', '2', 'comments', '1', '2017-01-27 12:17:47', '41', '2017-01-27 12:17:47', '41', '2017-01-27 12:17:47', '3');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1026', '31', '5', 'comments-manager', '1', '2017-01-27 12:17:47', '41', '2017-01-27 12:17:47', '41', '2017-01-27 12:17:47', '14');
INSERT INTO `pages` (`id`, `parent_id`, `templates_id`, `name`, `status`, `modified`, `modified_users_id`, `created`, `created_users_id`, `published`, `sort`) VALUES('1027', '1016', '46', 'recipes', '1', '2017-01-27 12:37:06', '41', '2017-01-27 12:37:06', '41', '2017-01-27 12:37:06', '4');
DROP TABLE IF EXISTS `pages_access`;
CREATE TABLE `pages_access` (
`pages_id` int(11) NOT NULL,
`templates_id` int(11) NOT NULL,
`ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`pages_id`),
KEY `templates_id` (`templates_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('32', '2', '2017-01-24 06:11:43');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('34', '2', '2017-01-24 06:11:43');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('35', '2', '2017-01-24 06:11:43');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('36', '2', '2017-01-24 06:11:43');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('37', '2', '2017-01-24 06:11:43');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('38', '2', '2017-01-24 06:11:43');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('50', '2', '2017-01-24 06:11:43');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('51', '2', '2017-01-24 06:11:43');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('52', '2', '2017-01-24 06:11:43');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('53', '2', '2017-01-24 06:11:43');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('54', '2', '2017-01-24 06:11:43');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1006', '2', '2017-01-24 06:11:43');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1010', '2', '2017-01-24 06:12:09');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1012', '2', '2017-01-24 06:12:17');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1013', '2', '2017-01-24 06:12:17');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1014', '1', '2017-01-25 15:22:52');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1015', '1', '2017-01-25 15:23:04');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1016', '1', '2017-01-26 05:54:06');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1017', '1', '2017-01-26 05:54:46');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1018', '1', '2017-01-26 05:54:53');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1019', '1', '2017-01-26 05:56:01');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1020', '1', '2017-01-26 06:10:41');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1021', '1', '2017-01-26 06:38:37');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1022', '1', '2017-01-26 09:50:20');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1023', '2', '2017-01-27 11:32:31');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1024', '1', '2017-01-27 12:01:56');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1026', '2', '2017-01-27 12:17:47');
INSERT INTO `pages_access` (`pages_id`, `templates_id`, `ts`) VALUES('1027', '1', '2017-01-27 12:37:06');
DROP TABLE IF EXISTS `pages_parents`;
CREATE TABLE `pages_parents` (
`pages_id` int(10) unsigned NOT NULL,
`parents_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`pages_id`,`parents_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('2', '1');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('3', '1');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('3', '2');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('7', '1');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('22', '1');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('22', '2');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('28', '1');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('28', '2');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('29', '1');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('29', '2');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('29', '28');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('30', '1');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('30', '2');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('30', '28');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('31', '1');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('31', '2');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('31', '28');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('1001', '1');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('1002', '1');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('1002', '1001');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('1004', '1');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('1004', '1001');
INSERT INTO `pages_parents` (`pages_id`, `parents_id`) VALUES('1005', '1');
DROP TABLE IF EXISTS `pages_sortfields`;
CREATE TABLE `pages_sortfields` (
`pages_id` int(10) unsigned NOT NULL DEFAULT '0',
`sortfield` varchar(20) NOT NULL DEFAULT '',
PRIMARY KEY (`pages_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `pages_sortfields` (`pages_id`, `sortfield`) VALUES('1016', 'name');
DROP TABLE IF EXISTS `session_login_throttle`;
CREATE TABLE `session_login_throttle` (
`name` varchar(128) NOT NULL,
`attempts` int(10) unsigned NOT NULL DEFAULT '0',
`last_attempt` int(10) unsigned NOT NULL,
PRIMARY KEY (`name`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `templates`;
CREATE TABLE `templates` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(191) CHARACTER SET ascii NOT NULL,
`fieldgroups_id` int(10) unsigned NOT NULL DEFAULT '0',
`flags` int(11) NOT NULL DEFAULT '0',
`cache_time` mediumint(9) NOT NULL DEFAULT '0',
`data` text NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`),
KEY `fieldgroups_id` (`fieldgroups_id`)
) ENGINE=MyISAM AUTO_INCREMENT=48 DEFAULT CHARSET=utf8;
INSERT INTO `templates` (`id`, `name`, `fieldgroups_id`, `flags`, `cache_time`, `data`) VALUES('1', 'home', '1', '0', '0', '{\"useRoles\":1,\"noParents\":1,\"slashUrls\":1,\"compile\":3,\"label\":\"Home\",\"modified\":1485537359,\"ns\":\"ProcessWire\",\"roles\":[37]}');
INSERT INTO `templates` (`id`, `name`, `fieldgroups_id`, `flags`, `cache_time`, `data`) VALUES('2', 'admin', '2', '8', '0', '{\"useRoles\":1,\"parentTemplates\":[2],\"allowPageNum\":1,\"redirectLogin\":23,\"slashUrls\":1,\"noGlobal\":1,\"compile\":3,\"modified\":1453457709,\"ns\":\"ProcessWire\"}');
INSERT INTO `templates` (`id`, `name`, `fieldgroups_id`, `flags`, `cache_time`, `data`) VALUES('3', 'user', '3', '8', '0', '{\"useRoles\":1,\"noChildren\":1,\"parentTemplates\":[2],\"slashUrls\":1,\"pageClass\":\"User\",\"noGlobal\":1,\"noMove\":1,\"noTrash\":1,\"noSettings\":1,\"noChangeTemplate\":1,\"nameContentTab\":1}');
INSERT INTO `templates` (`id`, `name`, `fieldgroups_id`, `flags`, `cache_time`, `data`) VALUES('4', 'role', '4', '8', '0', '{\"noChildren\":1,\"parentTemplates\":[2],\"slashUrls\":1,\"pageClass\":\"Role\",\"noGlobal\":1,\"noMove\":1,\"noTrash\":1,\"noSettings\":1,\"noChangeTemplate\":1,\"nameContentTab\":1}');
INSERT INTO `templates` (`id`, `name`, `fieldgroups_id`, `flags`, `cache_time`, `data`) VALUES('5', 'permission', '5', '8', '0', '{\"noChildren\":1,\"parentTemplates\":[2],\"slashUrls\":1,\"guestSearchable\":1,\"pageClass\":\"Permission\",\"noGlobal\":1,\"noMove\":1,\"noTrash\":1,\"noSettings\":1,\"noChangeTemplate\":1,\"nameContentTab\":1}');
INSERT INTO `templates` (`id`, `name`, `fieldgroups_id`, `flags`, `cache_time`, `data`) VALUES('26', 'search', '80', '0', '0', '{\"noChildren\":1,\"noParents\":1,\"allowPageNum\":1,\"slashUrls\":1,\"compile\":3,\"label\":\"Search\",\"modified\":1485526981,\"ns\":\"ProcessWire\"}');
INSERT INTO `templates` (`id`, `name`, `fieldgroups_id`, `flags`, `cache_time`, `data`) VALUES('29', 'basic-page', '83', '0', '0', '{\"slashUrls\":1,\"compile\":3,\"label\":\"Basic page\",\"modified\":1485526981,\"ns\":\"ProcessWire\"}');
INSERT INTO `templates` (`id`, `name`, `fieldgroups_id`, `flags`, `cache_time`, `data`) VALUES('34', 'sitemap', '88', '0', '0', '{\"noChildren\":1,\"noParents\":1,\"redirectLogin\":23,\"slashUrls\":1,\"compile\":3,\"label\":\"Sitemap\",\"modified\":1485427810,\"ns\":\"ProcessWire\"}');
INSERT INTO `templates` (`id`, `name`, `fieldgroups_id`, `flags`, `cache_time`, `data`) VALUES('43', 'blog-post', '97', '0', '0', '{\"parentTemplates\":[44],\"slashUrls\":1,\"compile\":3,\"label\":\"Blog post\",\"modified\":1485532830,\"ns\":\"ProcessWire\"}');
INSERT INTO `templates` (`id`, `name`, `fieldgroups_id`, `flags`, `cache_time`, `data`) VALUES('44', 'blog', '98', '0', '0', '{\"sortfield\":\"-97\",\"noParents\":-1,\"childTemplates\":[43],\"allowPageNum\":1,\"slashUrls\":1,\"compile\":3,\"label\":\"Blog\",\"modified\":1485530079,\"ns\":\"ProcessWire\"}');
INSERT INTO `templates` (`id`, `name`, `fieldgroups_id`, `flags`, `cache_time`, `data`) VALUES('45', 'categories', '99', '0', '0', '{\"noParents\":-1,\"childTemplates\":[46],\"slashUrls\":1,\"compile\":3,\"label\":\"Categories\",\"modified\":1485541446,\"ns\":\"ProcessWire\"}');
INSERT INTO `templates` (`id`, `name`, `fieldgroups_id`, `flags`, `cache_time`, `data`) VALUES('46', 'category', '100', '0', '0', '{\"noChildren\":1,\"parentTemplates\":[45],\"allowPageNum\":1,\"slashUrls\":1,\"compile\":3,\"label\":\"Category\",\"modified\":1485530079,\"ns\":\"ProcessWire\"}');
INSERT INTO `templates` (`id`, `name`, `fieldgroups_id`, `flags`, `cache_time`, `data`) VALUES('47', 'basic-page-edit', '101', '0', '0', '{\"slashUrls\":1,\"compile\":3,\"label\":\"Basic page (front-end editable)\",\"modified\":1485536717,\"ns\":\"ProcessWire\"}');
UPDATE pages SET created_users_id=41, modified_users_id=41, created=NOW(), modified=NOW();
# --- /WireDatabaseBackup {"numTables":26,"numCreateTables":33,"numInserts":416,"numSeconds":0}

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

View File

@@ -0,0 +1,88 @@
ABOUT /SITE/MODULES/
====================
This directory /site/modules/ is where you may install additional plugin modules.
These modules are specific to your site only. There is also a corresponding
/wire/modules/ directory, which contains ProcessWire's core modules (and best to
leave those alone).
If safe for your hosting environment, you may wish to make this directory
writable to PHP so that the installation of your modules can be managed from
ProcessWire's admin. However, this is not necessarily safe in all shared hosting
environments and is completely optional.
Where to get modules?
---------------------
Visit the modules directory at: http://modules.processwire.com
Installing modules from the ProcessWire admin
---------------------------------------------
If your /site/modules/ directory is writable, you can install modules from
ProcessWire's admin directly from the Modules Directory, from a ZIP file or from
a URL to a ZIP file. In your ProcessWire admin, see Modules > New for
installation options.
Installing modules from the file system
---------------------------------------
Each module (and any related files) should live in a directory of its own. The
directory should generally carry the same name as the module. For instance, if
you are installing a module named ProcessDatabaseBackups.module, then it should
live in the directory /site/modules/ProcessDatabaseBackups/.
Once you have placed a new module in this directory, you need to let ProcessWire
know about it. Login to the admin and click "Modules". Then click the "Check for
new modules" button. It will find your new module(s). Click the "Install" button
next to any new modules that you want to install.
Removing modules
----------------
The first step in removing a module is to uninstall it from ProcessWire (if it
isn't already). You do this by going to the "Modules" page, and "Site" tab in
your ProcessWire admin. Click the "Uninstall" button next to the module you
want to remove.
After the module is uninstalled, you may remove the module files. If your
modules file system is writable to ProcessWire, it will give you a "Delete"
button next to the module in your "Modules" admin page. You may click that to
remove the module files.
If your file system is not writable, you may remove the module files manually
from the file system (via SFTP or whatever tool you are using to manage your
files on the server).
Interested in learning how to make your own modules?
----------------------------------------------------
We've created two "Hello World" modules as examples for those interested in
learning module development:
- Helloworld.module demonstrates the basics of modules and hooks.
http://modules.processwire.com/modules/helloworld/
- ProcessHello.module demonstrates the basics of how to create a Process
module. Process modules are those that create applications in the admin.
http://modules.processwire.com/modules/process-hello/
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
--------------------
To find and download new modules, see the modules directory at:
http://modules.processwire.com/
For more information about modules, see the documentation at:
http://processwire.com/api/modules/
For discussion and support of modules, see:
http://processwire.com/talk/forum/4-modulesplugins/

45
site-regular/ready.php Normal file
View File

@@ -0,0 +1,45 @@
<?php namespace ProcessWire;
/**
* ProcessWire Bootstrap API Ready
* ===============================
* This ready.php file is called during ProcessWire bootstrap initialization process.
* This occurs after the current page has been determined and the API is fully ready
* to use, but before the current page has started rendering. This file receives a
* copy of all ProcessWire API variables. This file is an idea place for adding your
* own hook methods.
*
*/
/** @var ProcessWire $wire */
/**
* Example of a custom hook method
*
* This hook adds a “numPosts” method to pages using template “category”.
* The return value is the quantity of posts in category.
*
* Usage:
* ~~~~~
* $numPosts = $page->numPosts(); // returns integer
* numPosts = $page->numPosts(true); // returns string like "5 posts"
* ~~~~~
*
*/
$wire->addHook('Page(template=category)::numPosts', function($event) {
/** @var Page $page */
$page = $event->object;
// only category pages have numPosts
if($page->template != 'category') return;
// find number of posts
$numPosts = $event->pages->count("template=blog-post, categories=$page");
if($event->arguments(0) === true) {
// if true argument was specified, format it as a "5 posts" type string
$numPosts = sprintf(_n('%d post', '%d posts', $numPosts), $numPosts);
}
$event->return = $numPosts;
});

View File

@@ -0,0 +1,11 @@
<?php namespace ProcessWire;
/**
* This _init.php file is called automatically by ProcessWire before every page render
*
*/
/** @var ProcessWire $wire */
include_once('./_uikit.php');

View File

@@ -0,0 +1,137 @@
<?php namespace ProcessWire;
// _main.php template file, called after a pages template file
$home = pages()->get('/'); // homepage
$siteTitle = 'Regular';
$siteTagline = $home->summary;
// as a convenience, set location of our 3rd party resources (Uikit and jQuery)...
urls()->set('uikit', 'wire/modules/AdminTheme/AdminThemeUikit/uikit/dist/');
urls()->set('jquery', 'wire/modules/Jquery/JqueryCore/JqueryCore.js');
// ...or if you prefer to use CDN hosted resources, use these instead:
// urls()->set('uikit', 'https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.40/');
// urls()->set('jquery', 'https://code.jquery.com/jquery-2.2.4.min.js');
?><!DOCTYPE html>
<html lang='en'>
<head id='html-head'>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title id='html-title'><?=page()->title?></title>
<meta name="description" content="<?=page()->summary?>">
<link rel="stylesheet" href="<?=urls()->uikit?>css/uikit.min.css" />
<link rel="stylesheet" href="<?=urls()->templates?>styles/main.css">
<script src="<?=urls()->jquery?>"></script>
<script src="<?=urls()->uikit?>js/uikit.min.js"></script>
<script src="<?=urls()->uikit?>js/uikit-icons.min.js"></script>
</head>
<body id='html-body'>
<!-- MASTHEAD -->
<header class='uk-background-muted'>
<div id='masthead' class="uk-container">
<h2 id='masthead-logo' class='uk-text-center uk-margin-medium-top uk-margin-small-bottom'>
<a href='<?=urls()->root?>'>
<img src='<?=urls()->templates?>styles/images/coffee4.svg' alt='coffee'><br />
</a>
<?=$siteTitle?>
</h2>
<p id='masthead-tagline' class='uk-text-center uk-text-small uk-text-muted uk-margin-remove'>
<?=$siteTagline?>
</p>
<nav id='masthead-navbar' class="uk-navbar-container" uk-navbar>
<div class="uk-navbar-center uk-visible@m">
<?=ukNavbarNav($home->and($home->children), [
'dropdown' => [ 'basic-page', 'categories' ]
])?>
</div>
</nav>
</div>
</header>
<!-- MAIN CONTENT -->
<main id='main' class='uk-container uk-margin uk-margin-large-bottom'>
<?php if(page()->parent->id > $home->id) echo ukBreadcrumb(page(), [ 'class' => 'uk-visible@m' ]); ?>
<div class='uk-grid-large' uk-grid>
<div id='content' class='uk-width-expand'>
<h1 id='content-head' class='uk-margin-small-top'>
<?=page()->get('headline|title')?>
</h1>
<div id='content-body'>
<?=page()->body?>
</div>
</div>
<aside id='sidebar' class='uk-width-1-3@m'>
<?=page()->sidebar?>
</aside>
</div>
</main>
<?php if(config()->debug && user()->isSuperuser()): // display region debugging info ?>
<section id='debug' class='uk-section uk-section-muted'>
<div class='uk-container'>
<!--PW-REGION-DEBUG-->
</div>
</section>
<?php endif; ?>
<!-- FOOTER -->
<footer class='uk-section uk-section-secondary'>
<div id='footer' class='uk-container'>
<div uk-grid>
<div class='uk-width-1-3@m uk-flex-last@m uk-text-center'>
<form class='uk-search uk-search-default' action='<?=pages()->get('template=search')->url?>' method='get'>
<button type='submit' class='uk-search-toggle uk-search-icon-flip' uk-search-icon></button>
<input type='search' id='search-query' name='q' class='uk-search-input' placeholder='Search&hellip;'>
</form>
</div>
<div class='uk-width-2-3@m uk-flex-first@m uk-text-center uk-text-left@m'>
<h3 class='uk-margin-remove'>
<?=$siteTitle?>
<small class='uk-text-small uk-text-muted'><?=$siteTagline?></small>
</h3>
<p class='uk-margin-remove'>
<small class='uk-text-small uk-text-muted'>&copy; <?=date('Y')?> &bull;</small>
<a href='https://processwire.com'>Powered by ProcessWire CMS</a>
</p>
</div>
</div>
</div>
</footer>
<!-- OFFCANVAS NAV TOGGLE -->
<a id='offcanvas-toggle' class='uk-hidden@m' href="#offcanvas-nav" uk-toggle>
<?=ukIcon('menu', 1.3)?>
</a>
<!-- OFFCANVAS NAVIGATION -->
<div id="offcanvas-nav" uk-offcanvas>
<div class="uk-offcanvas-bar">
<h3><a href='<?=urls()->root?>'><?=$siteTitle?></a></h3>
<?php
// offcanvas navigation
// example of caching generated markup (for 600 seconds/10 minutes)
echo cache()->get('offcanvas-nav', 10, function() {
return ukNav(pages()->get('/')->children(), [
'depth' => 1,
'accordion' => true,
'blockParents' => [ 'blog' ],
'repeatParent' => true,
'noNavQty' => 20
]);
});
?>
</div>
</div>
<?php if(page()->editable): ?>
<!-- PAGE EDIT LINK -->
<a id='edit-page' href='<?=page()->editUrl?>'>
<?=ukIcon('pencil')?> Edit
</a>
<?php endif; ?>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
<?php namespace ProcessWire;
/**
* Admin template just loads the admin application controller,
* and admin is just an application built on top of ProcessWire.
*
* This demonstrates how you can use ProcessWire as a front-end
* to another application.
*
* Feel free to hook admin-specific functionality from this file,
* but remember to leave the require() statement below at the end.
*
*/
require($config->paths->adminTemplates . 'controller.php');

View File

@@ -0,0 +1,21 @@
<?php namespace ProcessWire;
// this template is very much like the basic-page template except that it
// demonstrates making the headline, body and sidebar fields editable on the
// front-end, using the <edit> tags
?>
<h1 id='content-head'>
<edit field='headline'><?=page()->headline?></edit>
</h1>
<div id='content-body'>
<edit field='body'><?=page()->body?></edit>
<?=ukDescriptionListPages(page()->children)?>
</div>
<aside id='sidebar'>
<?=ukNav(page()->rootParent, "depth=3, class=uk-margin-medium-bottom")?>
<edit field='sidebar'><?=page()->sidebar?></edit>
</aside>

View File

@@ -0,0 +1,10 @@
<?php namespace ProcessWire; ?>
<nav pw-append='content-body'>
<?=ukDescriptionListPages(page()->children)?>
</nav>
<aside id='sidebar' pw-prepend>
<?=ukNav(page()->rootParent, "depth=3")?>
</aside>

View File

@@ -0,0 +1,49 @@
<?php namespace ProcessWire; ?>
<head id='html-head' pw-append>
<script src='<?=urls()->FieldtypeComments?>comments.min.js'></script>
<link rel="stylesheet" href="<?=urls()->FieldtypeComments?>comments.css">
</head>
<div id='content'>
<?php
// blog post content
echo ukBlogPost(page());
// comments
$comments = page()->comments;
// comment list
if(count($comments)) {
echo ukHeading3("Comments", "icon=comments");
echo ukComments($comments);
}
// comment form
echo ukHeading3("Post a comment", "icon=comment");
echo ukCommentForm($comments);
// link to the next blog post, if there is one
$nextPost = page()->next();
if($nextPost->id): ?>
<p class='next-blog-post'>
Next <?=ukIcon('chevron-right')?>
<a href='<?=$nextPost->url?>'><?=$nextPost->title?></a>
</p>
<?php endif; ?>
</div>
<aside id='sidebar' pw-prepend>
<?php
$img = page()->images->first();
if($img) {
$img = $img->width(600);
echo "<p class='uk-text-center'><img src='$img->url' alt='$img->description'></p>";
}
?>
<?=ukNav(page()->parent->children('limit=10'), [ 'heading' => 'Recent blog posts' ])?>
<p><a href='<?=page()->parent->url?>'>More posts<?=ukIcon('arrow-right')?></a></p>
</aside>

View File

@@ -0,0 +1,20 @@
<?php namespace ProcessWire;
// This is the template file for main /blog/ page that lists blog post summaries.
// If there are more than 10 posts, it also paginates them.
?>
<div id='content'>
<?php
echo ukHeading1(page()->title, 'divider');
$posts = page()->children('limit=10');
echo ukBlogPosts($posts);
?>
</div>
<aside id='sidebar'>
<?php
$categories = pages()->get('/categories/');
echo ukNav($categories->children, [ 'header' => $categories->title ]);
?>
</aside>

View File

@@ -0,0 +1,16 @@
<?php namespace ProcessWire; ?>
<div class='uk-child-width-1-2@s uk-child-width-1-3@m uk-grid-match uk-margin-large-bottom' pw-append='content' uk-grid>
<?php foreach(page()->children as $category): ?>
<a class='uk-link-reset' href='<?=$category->url?>'>
<div class='uk-card uk-card-default uk-card-hover uk-card-body'>
<h3 class='uk-card-title uk-margin-remove'><?=$category->title?></h3>
<span class='uk-text-muted'><?=$category->numPosts(true)?></span>
</div>
</a>
<?php endforeach; ?>
</div>
<aside id='sidebar'>
<?=ukNav(pages()->get('/blog/')->children('limit=3'), [ 'header' => 'Recent posts' ])?>
</aside>

View File

@@ -0,0 +1,16 @@
<?php namespace ProcessWire; ?>
<div id='content'>
<?php
echo ukHeading1(page()->title, 'divider');
$posts = pages()->get('/blog/')->children("categories=$page, limit=10");
echo ukBlogPosts($posts);
?>
</div>
<aside id='sidebar'>
<?php
$categories = page()->parent->children();
echo ukNav($categories);
?>
</aside>

View File

@@ -0,0 +1,11 @@
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>
<head>
<title>500 Internal Server Error</title>
</head>
<body>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error or misconfiguration and was unable to complete your request.</p>
<p>{message}</p>
</body>
</html>

View File

@@ -0,0 +1,21 @@
When a fatal error occurs, ProcessWire displays the message:
"Unable to complete this request due to an error."
The message is intentionally vague for security purposes.
Details will be logged to /site/assets/logs/errors.txt.
When present in this directory, the file 500.html will be
displayed instead of the generic error message above. Feel
free to modify this file to show whatever you would like.
Please note the following:
* 500.html is plain HTML and has no PHP or API access.
* You may enter the tag {message} and ProcessWire will
replace this with additional details when applicable.
When not applicable, it will make it blank.
* If you are logged in as an admin, ProcessWire will
give you a detailed error message rather than 500.html.

View File

@@ -0,0 +1,32 @@
<?php namespace ProcessWire;
// get most recent blog post
$blog = pages()->get('/blog/');
$blogPost = $blog->child();
?>
<h1 id='content-head'>
<?=page()->headline?>
</h1>
<div class='uk-margin-top' id='content-body'>
<?=page()->body?>
<hr>
<p class='uk-margin-small'>
<a class='uk-button uk-button-link uk-link-muted' href='<?=$blog->url?>'>
In the blog
</a>
</p>
<?=ukBlogPost($blogPost)?>
<p class='uk-margin-small'>
<a href='<?=$blog->url?>'>More blog posts <?=ukIcon('arrow-right')?></a>
</p>
</div>
<aside id='sidebar'>
<?=ukNav(pages()->get('/categories/')->children)?>
<div class='uk-card uk-card-default uk-card-hover uk-card-body uk-margin-medium-top'>
<?=page()->sidebar?>
</div>
</aside>

View File

@@ -0,0 +1,4 @@
We typically use this directory for javascript files,
but this site profile does not have any at present.
This file is a placeholder so that this directory
exists in Git.

View File

@@ -0,0 +1,49 @@
<?php namespace ProcessWire;
// look for a GET variable named 'q' and sanitize it
$q = input()->get('q');
// sanitize to text, which removes markup, newlines, too long, etc.
$q = sanitizer()->text($q);
// did $q have anything in it after sanitizing to text?
if($q) {
// Make the search query appear in the top-right search box.
// Always entity encode any user input that also gets output
echo '<input id="search-query" value="' . sanitizer()->entities($q) . '">';
// Sanitize for placement within a selector string. This is important for any
// values that you plan to bundle in a selector string like we are doing here.
// It quotes them when necessary, and removes characters that might cause issues.
$q = sanitizer()->selectorValue($q);
// Search the title and body fields for our query text.
// Limit the results to 50 pages. The has_parent!=2 excludes irrelevant admin
// pages from the search, for when an admin user performs a search.
$selector = "title|body~=$q, limit=50, has_parent!=2";
// Find pages that match the selector
$matches = pages()->find($selector);
} else {
$matches = array();
}
// unset the variable that we no longer need, since it can contain user input
unset($q);
?>
<div id='content-body'>
<?php
// did we find any matches?
if(count($matches)) {
// yes we did, render them
echo ukAlert("Found $matches->count page(s)", "default", "search");
echo ukDescriptionListPages($matches);
} else {
// we didn't find any
echo ukAlert("Sorry, no results were found.", "danger", "warning");
}
?>
</div>

View File

@@ -0,0 +1,9 @@
<?php namespace ProcessWire; ?>
<div id='content-body'>
<?php
$home = pages()->get('/');
echo ukNav($home, [ 'depth' => 4 ])
?>
</div>

View File

@@ -0,0 +1,382 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:ns1="http://sozi.baierouge.fr"
id="svg2"
sodipodi:docname="coffeecup-3d.svg"
inkscape:export-filename="D:\mocholand\Development\vectorial-cofee-cup\coffeecup-3d.png"
viewBox="0 0 444.47 310.87"
inkscape:export-xdpi="90"
version="1.1"
inkscape:export-ydpi="90"
inkscape:version="0.47pre3 "
>
<defs
id="defs4"
>
<radialGradient
id="radialGradient4309"
gradientUnits="userSpaceOnUse"
cy="351.05"
cx="368.58"
gradientTransform="matrix(1 0 0 .54810 0 141.63)"
r="185.64"
inkscape:collect="always"
>
<stop
id="stop4197"
style="stop-color:#e2e2e2"
offset="0"
/>
<stop
id="stop4199"
style="stop-color:#989898"
offset="1"
/>
</radialGradient
>
<linearGradient
id="linearGradient4311"
y2="321.49"
gradientUnits="userSpaceOnUse"
x2="369.95"
y1="346.56"
x1="369.95"
inkscape:collect="always"
>
<stop
id="stop4220"
style="stop-color:#838383"
offset="0"
/>
<stop
id="stop4222"
style="stop-color:#c6c6c6;stop-opacity:0"
offset="1"
/>
</linearGradient
>
<linearGradient
id="linearGradient4313"
y2="378.81"
gradientUnits="userSpaceOnUse"
x2="388.13"
gradientTransform="translate(0 -60)"
y1="327.72"
x1="368.58"
inkscape:collect="always"
>
<stop
id="stop4265"
style="stop-color:#461f00"
offset="0"
/>
<stop
id="stop4267"
style="stop-color:#753300"
offset="1"
/>
</linearGradient
>
<linearGradient
id="linearGradient4315"
y2="327.72"
gradientUnits="userSpaceOnUse"
x2="368.58"
gradientTransform="translate(0 -60)"
y1="362.58"
x1="368.58"
inkscape:collect="always"
>
<stop
id="stop4275"
style="stop-color:#ffffff"
offset="0"
/>
<stop
id="stop4277"
style="stop-color:#ffffff;stop-opacity:0"
offset="1"
/>
</linearGradient
>
<filter
id="filter4335"
height="2.8094"
width="1.3596"
color-interpolation-filters="sRGB"
y="-.90469"
x="-.17981"
inkscape:collect="always"
>
<feGaussianBlur
id="feGaussianBlur4337"
stdDeviation="12.0625"
inkscape:collect="always"
/>
</filter
>
<radialGradient
id="radialGradient4339"
gradientUnits="userSpaceOnUse"
cy="582.55"
cx="408.1"
gradientTransform="matrix(1 0 0 .18691 0 409.16)"
r="222.23"
inkscape:collect="always"
>
<stop
id="stop4109"
style="stop-color:#e9e9e9"
offset="0"
/>
<stop
id="stop4119"
style="stop-color:#b3b3b3"
offset="1"
/>
</radialGradient
>
</defs
>
<sodipodi:namedview
id="base"
bordercolor="#666666"
inkscape:pageshadow="2"
inkscape:window-y="-4"
pagecolor="#ffffff"
inkscape:window-height="816"
inkscape:window-maximized="1"
inkscape:zoom="1"
inkscape:window-x="-4"
showgrid="false"
borderopacity="1.0"
inkscape:current-layer="layer1"
inkscape:cx="238.91057"
inkscape:cy="148.53567"
inkscape:window-width="1152"
inkscape:pageopacity="0.0"
inkscape:document-units="px"
/>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer"
transform="translate(-145.59 -250.53)"
>
<path
id="path4182"
sodipodi:rx="222.23357"
sodipodi:ry="62.629459"
style="fill-rule:evenodd;fill:#595959"
sodipodi:type="arc"
d="m630.34 492.74c0 34.589-99.497 62.629-222.23 62.629-122.74 0-222.23-28.04-222.23-62.629s99.497-62.629 222.23-62.629c122.74 0 222.23 28.04 222.23 62.629z"
transform="matrix(.94938 0 0 1.0617 -20.871 -28.258)"
sodipodi:cy="492.73767"
sodipodi:cx="408.10162"
/>
<path
id="path4159"
sodipodi:rx="222.23357"
sodipodi:ry="62.629459"
style="fill-rule:evenodd;fill:#f2f2f2"
sodipodi:type="arc"
d="m630.34 492.74c0 34.589-99.497 62.629-222.23 62.629-122.74 0-222.23-28.04-222.23-62.629s99.497-62.629 222.23-62.629c122.74 0 222.23 28.04 222.23 62.629z"
transform="translate(-40.279 -1.7029)"
sodipodi:cy="492.73767"
sodipodi:cx="408.10162"
/>
<path
id="path3333"
sodipodi:rx="222.23357"
sodipodi:ry="62.629459"
style="fill-rule:evenodd;fill:url(#radialGradient4339)"
sodipodi:type="arc"
d="m630.34 492.74c0 34.589-99.497 62.629-222.23 62.629-122.74 0-222.23-28.04-222.23-62.629s99.497-62.629 222.23-62.629c122.74 0 222.23 28.04 222.23 62.629z"
transform="translate(-40.279 -5.7029)"
sodipodi:cy="492.73767"
sodipodi:cx="408.10162"
/>
<path
id="path4186"
sodipodi:rx="123.3854"
sodipodi:ry="29.52813"
style="fill-opacity:.40698;fill:#858585"
sodipodi:type="arc"
d="m487.21 550.38c0 16.308-55.242 29.528-123.39 29.528-68.144 0-123.39-13.22-123.39-29.528s55.242-29.528 123.39-29.528c68.144 0 123.39 13.22 123.39 29.528z"
transform="translate(6 -66)"
sodipodi:cy="550.38397"
sodipodi:cx="363.82874"
/>
<path
id="path4317"
sodipodi:rx="80.5"
sodipodi:ry="16"
style="opacity:.75;filter:url(#filter4335);fill:#000000"
sodipodi:type="arc"
d="m448 483.36c0 8.8366-36.041 16-80.5 16s-80.5-7.1634-80.5-16 36.041-16 80.5-16 80.5 7.1634 80.5 16z"
transform="matrix(.93409 0 0 .91146 23.008 44.818)"
sodipodi:cy="483.36218"
sodipodi:cx="367.5"
/>
<g
id="g4295"
transform="translate(0,4)"
>
<path
id="path4188"
style="fill:url(#radialGradient4309)"
d="m182.94 284.44c18.733 116.39 94.715 203.5 185.62 203.5 90.911 0 166.92-87.107 185.66-203.5h-371.28z"
/>
<path
id="path4193"
sodipodi:rx="181.46394"
sodipodi:ry="25.064081"
style="fill:#eaeaea"
sodipodi:type="arc"
d="m551.41 346.56c0 13.843-81.244 25.064-181.46 25.064s-181.46-11.222-181.46-25.064c0-13.843 81.244-25.064 181.46-25.064s181.46 11.222 181.46 25.064z"
transform="matrix(1.0234 0 0 1.6116 -10.017 -271.57)"
sodipodi:cy="346.55768"
sodipodi:cx="369.94583"
/>
<path
id="path4216"
sodipodi:rx="181.46394"
sodipodi:ry="25.064081"
style="fill:url(#linearGradient4311)"
sodipodi:type="arc"
d="m551.41 346.56c0 13.843-81.244 25.064-181.46 25.064s-181.46-11.222-181.46-25.064c0-13.843 81.244-25.064 181.46-25.064s181.46 11.222 181.46 25.064z"
transform="matrix(.98128 0 0 1.4117 5.5602 -205.8)"
sodipodi:cy="346.55768"
sodipodi:cx="369.94583"
/>
<path
id="path4255"
style="fill:url(#linearGradient4313)"
d="m368.59 267.72c-91.89 0-166.42 12.631-166.62 28.219 25.429 13.369 90.44 22.875 166.62 22.875 76.175 0 141.16-9.5089 166.59-22.875-0.2008-15.588-74.704-28.219-166.59-28.219z"
/>
<path
id="path4271"
sodipodi:nodetypes="cccc"
style="opacity:0.04;fill:url(#linearGradient4315)"
d="m368.59 267.72c-87.769 0-159.71 11.539-166.16 26.156 107.47-8.4524 217.07-11.433 332.28 0-6.4479-14.617-78.356-26.156-166.12-26.156z"
/>
<path
id="path4293"
sodipodi:nodetypes="cccc"
style="fill-opacity:.15116;fill:#ffffff"
d="m213.75 309.23c9.0001 2.9648 14.241 3.9652 20.516 5.5258 0 0 1.2916 65.75 23.592 103.41-24.432-33.615-44.951-65.463-44.108-108.94z"
/>
</g
>
</g
>
<metadata
>
<rdf:RDF
>
<cc:Work
>
<dc:format
>image/svg+xml</dc:format
>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage"
/>
<cc:license
rdf:resource="http://creativecommons.org/licenses/publicdomain/"
/>
<dc:publisher
>
<cc:Agent
rdf:about="http://openclipart.org/"
>
<dc:title
>Openclipart</dc:title
>
</cc:Agent
>
</dc:publisher
>
<dc:title
>Realistic Coffee cup - Front 3D view</dc:title
>
<dc:date
>2009-10-17T06:56:28</dc:date
>
<dc:description
>Realistic Coffee cup, with a shiny 3d look, front view.</dc:description
>
<dc:source
>https://openclipart.org/detail/27828/realistic-coffee-cup---front-3d-view-by-mokush</dc:source
>
<dc:creator
>
<cc:Agent
>
<dc:title
>mokush</dc:title
>
</cc:Agent
>
</dc:creator
>
<dc:subject
>
<rdf:Bag
>
<rdf:li
>cofe</rdf:li
>
<rdf:li
>cofee</rdf:li
>
<rdf:li
>coffe</rdf:li
>
<rdf:li
>coffee</rdf:li
>
<rdf:li
>coffeecup</rdf:li
>
<rdf:li
>cup</rdf:li
>
<rdf:li
>photorealistic</rdf:li
>
</rdf:Bag
>
</dc:subject
>
</cc:Work
>
<cc:License
rdf:about="http://creativecommons.org/licenses/publicdomain/"
>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
/>
</cc:License
>
</rdf:RDF
>
</metadata
>
</svg
>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,111 @@
#html-body, h1, h2, h3, h4, h5 {
/* default font */
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
#masthead-logo img {
/* image for masthead logo */
width: 100px;
}
#edit-page {
/* the edit page link that appears when page is editable */
position: absolute;
top: 10px;
right: 10px
}
#offcanvas-toggle {
/* the hamburger icon that toggles the mobile navigation */
position: absolute;
top: 10px;
left: 15px;
}
#offcanvas-nav .uk-search-input {
/* the search box that appears in offcanvas nav */
width: 100%;
}
img.uk-comment-avatar {
/* avatar that appears in comments */
width: 60px;
height: 60px;
}
@media only screen and (max-width: 959px) {
/* custom adjustments for mobile layouts under 960px */
#masthead {
/* primary nav is not visible, so it needs some padding here */
padding-bottom: 20px;
}
}
/****************************************************************
* Bodycopy text and images
*
* These styles are good to have on any ProcessWire site
*
*/
.align_left {
/* for images placed in rich text editor */
float: left;
margin: 0 1em 0.5em 0;
position: relative;
top: 0.5em;
max-width: 50%;
}
.align_right {
/* for images placed in rich text editor */
float: right;
margin: 0 0 0.5em 1em;
max-width: 50%;
}
.align_center {
/* for images placed in rich text editor */
display: block;
margin: 1em auto;
position: relative;
top: 0.5em;
}
figure {
/* figure for image that has a caption */
display: table;
margin: 1em 0;
}
figure figcaption {
/* display caption text below image contained to image width */
display: table-caption;
caption-side: bottom;
font-size: 13px;
line-height: 1.4em;
margin-top: 5px;
color: #777;
}
@media only screen and (max-width: 767px) {
/* common PW mobile layout adjustments for widths under 768px */
.align_left, .align_right, .align_center {
/* display images in center rather than aligned */
display: block;
float: none;
margin: 1em auto;
max-width: 100%;
}
figure,
figure figcaption {
/* let figcaption display as wide as needed below image */
display: block;
text-align: center;
}
}

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

@@ -142,6 +142,25 @@ $config->useFunctionsAPI = false;
*/
$config->useMarkupRegions = false;
/**
* Disable all HTTPS requirements?
*
* Use this option only for development or staging environments, on sites where you are
* otherwise requiring HTTPS. By setting this option to something other than false, you
* can disable any HTTPS requirements specified per-template, enabling you to use your
* site without SSL during development or staging, etc.
*
* The following options are available:
* - boolean true: Disable HTTPS requirements globally
* - string containing hostname: Disable HTTPS requirements only for specified hostname.
* - array containing hostnames: Disable HTTPS requirements for specified hostnames.
*
* @var bool|string|array
*
*/
$config->noHTTPS = false;
/*** 2. DATES & TIMES *************************************************************************/
@@ -331,6 +350,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 **************************************************************************/
@@ -544,6 +581,7 @@ $config->fileContentTypes = array(
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/x-png',
'svg' => 'image/svg+xml'
);
@@ -554,7 +592,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)
@@ -566,6 +605,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
@@ -698,6 +738,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
*
@@ -758,6 +806,16 @@ $config->pageNameCharset = 'ascii';
*/
$config->pageNameWhitelist = '-_.abcdefghijklmnopqrstuvwxyz0123456789æåäßöüđжхцчшщюяàáâèéëêěìíïîõòóôøùúûůñçčćďĺľńňŕřšťýžабвгдеёзийклмнопрстуфыэęąśłżź';
/**
* Name to use for untitled pages
*
* When page has this name, the name will be changed automatically (to a field like title) when it is possible to do so.
*
* @var string
*
*/
$config->pageNameUntitled = "untitled";
/**
* Maximum paginations
*
@@ -783,11 +841,55 @@ $config->maxPageNum = 999;
*/
$config->wireInputOrder = 'get post';
/**
* Lazy-load get/post/cookie input into $input API var?
*
* This is an experimental option for reduced memory usage when a lot of input data is present.
*
* This prevents PW from keeping separate copies of get/post/cookie data, and it instead works
* directly from the PHP $_GET, $_POST and $_COOKIE vars.
*
* This option is also useful in that anything you SET to PWs $input->get/post/cookie also gets
* set to the equivalent PHP $_GET, $_POST and $_COOKIE.
*
* @var bool
*
*/
$config->wireInputLazy = false;
/*** 7. DATABASE ********************************************************************************/
/**
* Database name
*
*/
$config->dbName = '';
/**
* Database username
*
*/
$config->dbUser = '';
/**
* Database password
*
*/
$config->dbPass = '';
/**
* Database host
*
*/
$config->dbHost = '';
/**
* Database port
*
*/
$config->dbPort = 3306;
/**
* Database character set
*
@@ -842,30 +944,6 @@ $config->dbPath = '';
*/
$config->dbLowercaseTables = true;
/**
* Database username
*
*/
$config->dbUser = '';
/**
* Database password
*
*/
$config->dbPass = '';
/**
* Database host
*
*/
$config->dbHost = '';
/**
* Database port
*
*/
$config->dbPort = 3306;
/**
* Database init command (PDO::MYSQL_ATTR_INIT_COMMAND)
*
@@ -982,6 +1060,29 @@ $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 string newline What to use for newline if different from RFC standard of "\r\n" (optional).
* #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
*
@@ -1025,6 +1126,16 @@ $config->pageEdit = array(
'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 ************************************************************************************/
@@ -1044,12 +1155,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
@@ -1187,6 +1306,7 @@ $config->lazyPageChunkSize = 250;
*
*/
/*** 10. RUNTIME ********************************************************************************
*
* The following are runtime-only settings and cannot be changed from /site/config.php
@@ -1242,9 +1362,17 @@ $config->versionName = '';
* Value is null, 0, or 1 or higher. This should be kept at null in this file.
*
*/
$config->inputfieldColumnWidthSpacing = null;
$config->inputfieldColumnWidthSpacing = null;
/**
* Populated to contain <link rel='next|prev'.../> tags for document head
*
* This is populated only after a MarkupPagerNav::render() has rendered pagination and is
* otherwise null.
*
* $config->pagerHeadTags = '';
*
*/
/*** 11. SYSTEM *********************************************************************************
*

View File

@@ -10,7 +10,7 @@
* This file is licensed under the MIT license.
* https://processwire.com/about/license/mit/
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
* @property int|string $version Current admin theme version
@@ -62,6 +62,14 @@ abstract class AdminTheme extends WireData implements Module {
*/
protected $bodyClasses = array();
/**
* General purpose classes indexed by name
*
* @var array
*
*/
protected $classes = array();
/**
* Extra markup regions
*
@@ -95,9 +103,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() {
@@ -116,6 +126,11 @@ abstract class AdminTheme extends WireData implements Module {
// if admin theme has already been set, then no need to continue
if($this->wire('adminTheme')) return;
/** @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) {
@@ -129,19 +144,20 @@ abstract class AdminTheme extends WireData implements Module {
}
// 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);
@@ -224,7 +240,7 @@ abstract class AdminTheme extends WireData implements Module {
*
*/
public function addBodyClass($className) {
$this->bodyClasses[$className] = $className;
$this->addClass('body', $className);
}
/**
@@ -234,7 +250,75 @@ abstract class AdminTheme extends WireData implements Module {
*
*/
public function getBodyClass() {
return trim(implode(' ', $this->bodyClasses));
return $this->getClass('body');
}
/**
* Return class for a given named item or blank if none available
*
* Omit the first argument to return all classes in an array.
*
* @param string $name Tag or item name, i.e. “input”, or omit to return all defined [tags=classes]
* @param bool $getArray Specify true to return array of class name(s) rather than string (default=false). $tagName argument required.
* @return string|array Returns string or array of class names, or array of all [tags=classes] or $tagName argument is empty.
*
*/
public function getClass($name = '', $getArray = false) {
if(empty($name)) {
return $this->classes;
} else if(isset($this->classes[$name])) {
return $getArray ? explode(' ', $this->classes[$name]) : $this->classes[$name];
} else {
return $getArray ? array() : '';
}
}
/**
* Add class for given named item
*
* Default behavior is to merge classes if existing classes are already present for given item $name.
*
* #pw-internal
*
* @param string $name
* @param string|array $class
* @param bool $replace Specify true to replace any existing classes rather than merging them
*
*/
public function addClass($name, $class, $replace = false) {
if(is_array($class)) {
foreach($class as $c) {
$this->addClass($name, $c);
}
} else if(!$replace && isset($this->classes[$name])) {
$classes = $this->classes[$name];
if(strpos($classes, $class) !== false) {
// avoid re-adding class if it is already present
if(array_search($class, explode(' ', $classes)) !== false) return;
}
$this->classes[$name] = trim($classes . ' ' . ltrim($class));
} else {
$this->classes[$name] = trim($class);
}
}
/**
* Set classes for multiple tags
*
* #pw-internal
*
* @param array $classes Array of strings (class names) where keys are tag names
* @param bool $replace Specify true to replace any existing classes rather than merge them (default=false)
*
*/
public function setClasses(array $classes, $replace = false) {
if($replace || empty($this->classes)) {
$this->classes = $classes;
} else {
foreach($classes as $name => $class) {
$this->addClass($name, $class);
}
}
}
/**
@@ -269,7 +353,7 @@ abstract class AdminTheme extends WireData implements Module {
try {
$field->save();
} catch(\Exception $e) {
$this->error("Error creating 'admin_theme' field: " . $e->getMessage());
// $this->error("Error creating 'admin_theme' field: " . $e->getMessage());
}
}
@@ -304,6 +388,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

@@ -9,7 +9,7 @@
* @property bool $isSuperuser
* @property bool $isEditor
* @property bool $isLoggedIn
* @property bool $isModal
* @property bool|string $isModal
* @property bool|int $useAsLogin
* @method array getUserNavArray()
*
@@ -84,6 +84,8 @@ abstract class AdminThemeFramework extends AdminTheme {
/**
* Initialize and attach hooks
*
* Note: descending classes should call this after API ready
*
*/
public function init() {
@@ -98,13 +100,32 @@ abstract class AdminThemeFramework extends AdminTheme {
$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->testNotices();
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
@@ -131,6 +152,7 @@ abstract class AdminThemeFramework extends AdminTheme {
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);
}
@@ -301,8 +323,10 @@ abstract class AdminThemeFramework extends AdminTheme {
if($this->isSuperuser) return true;
$allow = false;
$pageViewable = $p->viewable();
if(!$pageViewable) return false;
$allow = false;
$numChildren = count($children);
if($p->process == 'ProcessPageAdd') {
@@ -555,10 +579,12 @@ abstract class AdminThemeFramework extends AdminTheme {
/**
* Test all notice types
*
* @return bool
*
*/
public function testNotices() {
if(!$this->wire('user')->isLoggedin()) return;
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);
@@ -568,25 +594,19 @@ abstract class AdminThemeFramework extends AdminTheme {
$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 $notices
* @param array $options See defaults in method
* @return string
* @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()) {
if(!count($notices)) return '';
if($this->isLoggedIn && $this->wire('modules')->isInstalled('SystemNotifications')) {
$systemNotifications = $this->wire('modules')->get('SystemNotifications');
if(!$systemNotifications->placement) return '';
}
$defaults = array(
'messageClass' => 'NoticeMessage', // class for messages
'messageIcon' => 'check-square', // default icon to show with notices
@@ -596,65 +616,117 @@ abstract class AdminThemeFramework extends AdminTheme {
'errorIcon' => 'exclamation-triangle', // icon for errors
'debugClass' => 'NoticeDebug', // class for debug items (appended)
'debugIcon' => 'bug', // icon for debug notices
'closeClass' => 'notice-remove', // class for close notices link <a>
'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>"
'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;
if($notice->flags & Notice::allowMarkup) {
$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);
$text = nl2br($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";
// show remove link for first item only
if($n) {
$remove = '';
} else {
$removeIcon = $this->renderIcon($options['closeIcon']);
$removeLabel = $this->_('Close all');
$remove = "<a class='$options[closeClass]' href='#' title='$removeLabel'>$removeIcon</a>";
}
$replacements = array(
'{class}' => $class,
'{remove}' => $remove,
'{remove}' => '',
'{icon}' => $this->renderNavIcon($notice->icon ? $notice->icon : $icon),
'{text}' => $text,
);
$out .= str_replace(array_keys($replacements), array_values($replacements), $options['itemMarkup']);
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']);
@@ -720,7 +792,7 @@ abstract class AdminThemeFramework extends AdminTheme {
$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) {

View File

@@ -210,7 +210,7 @@ class CacheFile extends Wire {
foreach($dir as $file) {
if($file->isDir() || $file->isDot()) continue;
//if(strpos($file->getFilename(), self::cacheFileExtension)) @unlink($file->getPathname());
if(self::isCacheFile($file->getPathname())) @unlink($file->getPathname());
if(self::isCacheFile($file->getPathname())) $this->wire('files')->unlink($file->getPathname());
}
return @rmdir($this->path);
@@ -223,7 +223,7 @@ class CacheFile extends Wire {
*
*/
protected function removeFilename($filename) {
@unlink($filename);
$this->wire('files')->unlink($filename);
}
@@ -250,7 +250,7 @@ class CacheFile extends Wire {
$numRemoved += self::removeAll($pathname, true);
} else if($file->isFile() && (self::isCacheFile($pathname) || ($file->getFilename() == self::globalExpireFilename))) {
if(unlink($pathname)) $numRemoved++;
if(wire('files')->unlink($pathname)) $numRemoved++;
}
}

View File

@@ -39,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
@@ -63,6 +63,7 @@
* @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? 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
@@ -76,11 +77,14 @@
*
* @property string $pageNameCharset Character set for page names, must be 'ascii' (default, lowercase) or 'UTF8' (uppercase). #pw-group-URLs
* @property string $pageNameWhitelist Whitelist of characters allowed in UTF8 page names. #pw-group-URLs
* @property string $pageNameUntitled Name to use for untitled pages (default="untitled"). #pw-group-URLs
* @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 $wireInputLazy Specify true for $input API var to load input data in a lazy fashion and potentially use less memory. Default is false. #pw-group-HTTP-and-input
*
* @property 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
@@ -93,6 +97,7 @@
* @property array $adminThumbOptions Admin thumbnail image options #pw-group-images
* @property array $httpHosts HTTP hosts For added security, specify the host names ProcessWire should recognize. #pw-group-HTTP-and-input
* @property int $maxPageNum Maximum number of recognized paginations #pw-group-URLs
* @property bool|string|array $noHTTPS When boolean true, pages requiring HTTPS will not enforce it (useful for dev environments). May also specify hostname (string) or hostnames (array) to disable HTTPS for. #pw-group-HTTP-and-input
*
* @property string $dbHost Database host #pw-group-database
* @property string $dbName Database name #pw-group-database
@@ -113,12 +118,15 @@
*
* @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 bool $logIP Include IP address in logs, when applicable? #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
* @property array $modals Settings for modal windows #pw-group-admin
@@ -138,6 +146,7 @@
* @property string $versionName This is automatically populated with the current PW version name (i.e. 2.5.0 dev) #pw-group-runtime
* @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 string|null $pagerHeadTags Populated at runtime to contain `<link rel=prev|next />` tags for document head, after pagination has been rendered by MarkupPagerNav module. #pw-group-runtime
*
* @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
@@ -182,7 +191,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
*
*/
@@ -195,7 +204,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
*
*/
@@ -344,6 +353,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
@@ -378,7 +390,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) {
@@ -388,5 +407,45 @@ class Config extends WireData {
return $this;
}
/**
* Return true if current PHP version is equal to or newer than the given version
*
* ~~~~~
* if($config->phpVersion('7.0.0')) {
* // PHP version is 7.x
* }
* ~~~~~
*
* @param string|null $minVersion
* @return bool
* @since 3.0.101
*
*/
public function phpVersion($minVersion) {
return version_compare(PHP_VERSION, $minVersion) >= 0;
}
/**
* Check if current ProcessWire version is equal to or newer than given versino
*
* If no version argument is given, it simply returns the current ProcessWire version.
*
* ~~~~~
* if($config->version('3.0.100')) {
* // ProcessWire version is 3.0.100 or newer
* }
* ~~~~~
*
* @param string $minVersion Specify version string if you want to compare against current version
* @return bool|string Returns current version if no argument given, OR boolean if given a version argument:
* - If given version is older than current, returns false.
* - If given version is equal to or newer than current, returns true.
* @since 3.0.106
*
*/
public function version($minVersion = '') {
return version_compare($this->version, $minVersion) >= 0;
}
}

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

@@ -18,7 +18,7 @@
*
* @property array $where
* @property array $bindValues
* @properety array $bindIndex
* @property array $bindIndex
*
*/
abstract class DatabaseQuery extends WireData {
@@ -216,6 +216,9 @@ abstract class DatabaseQuery extends WireData {
/**
* Execute the query with the current database handle
*
* @return \PDOStatement
* @throws WireException|\Exception|\PDOException
*
*/
public function execute() {

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

@@ -8,7 +8,7 @@
* This file is licensed under the MIT license
* https://processwire.com/about/license/mit/
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
*/
@@ -20,21 +20,51 @@
class WireException extends \Exception {}
/**
* Triggered when access to a resource is not allowed
* Thrown when access to a resource is not allowed
*
*/
class WirePermissionException extends WireException {}
/**
* Triggered when a requested item does not exist and generates a fatal error
* Thrown when a requested page does not exist, or can be thrown manually to show the 404 page
*
*/
class Wire404Exception extends WireException {}
/**
* WireDatabaseException is the exception thrown by the Database class
*
* If you use this class without ProcessWire, change 'extends WireException' below to be just 'extends Exception'
* Thrown when ProcessWire is unable to connect to the database at boot
*
*/
class WireDatabaseException extends WireException {}
/**
* Thrown when cross site request forgery detected by SessionCSRF::validate()
*
*/
class WireCSRFException extends WireException {}
/**
* Thrown when a requested Process or Process method is requested that doesnt exist
*
*/
class ProcessController404Exception extends Wire404Exception { }
/**
* Thrown when the user doesnt have access to execute the requested Process or method
*
*/
class ProcessControllerPermissionException extends WirePermissionException { }
/**
* Thrown by PageFinder when an error occurs trying to find pages
*
*/
class PageFinderException extends WireException { }
/**
* Thrown by PageFinder when it detects an error in the syntax of a given page-finding selector
*
*/
class PageFinderSyntaxException extends PageFinderException { }

View File

@@ -12,7 +12,7 @@
* #pw-body Field objects are managed by the `$fields` API variable.
* #pw-use-constants
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
* @property int $id Numeric ID of field in the database #pw-group-properties
@@ -26,11 +26,14 @@
* @property string $description Longer description text for the field #pw-group-properties
* @property string $notes Additional notes text about the field #pw-group-properties
* @property string $icon Icon name used by the field, if applicable #pw-group-properties
* @property string $tags Tags that represent this field, if applicable (space separated string). #pw-group-properties
* @property-read array $tagList Same as $tags property, but as an array. #pw-group-properties
* @property bool $useRoles Whether or not access control is enabled #pw-group-access
* @property array $editRoles Role IDs with edit access, applicable only if access control is enabled. #pw-group-access
* @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 Whether or not this field is required during input #pw-group-properties
@@ -202,6 +205,14 @@ class Field extends WireData implements Saveable, Exportable {
*/
protected $inputfieldSettings = array();
/**
* Tags assigned to this field, keys are lowercase version of tag, values can possibly contain mixed case
*
* @var null|array
*
*/
protected $tagList = null;
/**
* True if lowercase tables should be enforce, false if not (null = unset). Cached from $config
*
@@ -340,17 +351,26 @@ class Field extends WireData implements Saveable, Exportable {
*
*/
public function get($key) {
if($key === 'type' && isset($this->settings['type'])) {
$value = $this->settings['type'];
if($value) $value->setLastAccessField($this);
return $value;
}
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'];
else if($key == 'tagList') return $this->getTags();
else if($key == 'tags') return $this->getTags(true);
$value = parent::get($key);
if($key === 'allowContexts' && !is_array($value)) $value = array();
if(is_array($this->trackGets)) $this->trackGets($key);
return $value;
}
@@ -446,6 +466,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;
}
@@ -495,7 +529,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
@@ -630,6 +668,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') {
@@ -812,7 +859,8 @@ class Field extends WireData implements Saveable, Exportable {
// 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('hasField', $this);
$inputfield->set('hasPage', $page);
// custom field settings
foreach($this->data as $key => $value) {
@@ -915,6 +963,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();
}
@@ -925,6 +974,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);
@@ -939,7 +990,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);
}
@@ -954,13 +1017,18 @@ 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');
/** @var InputfieldWrapper $inputfieldInputfields */
$inputfieldInputfields = $inputfield->getConfigInputfields();
if(!$inputfieldInputfields) $inputfieldInputfields = $this->wire(new InputfieldWrapper());
$configArray = $inputfield->getConfigArray();
@@ -973,6 +1041,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]);
}
}
}
@@ -1190,6 +1268,104 @@ class Field extends WireData implements Saveable, Exportable {
return $this;
}
/**
* Get tags
*
* @param bool|string $getString Optionally specify true for space-separated string, or delimiter string (default=false)
* @return array|string Returns array of tags unless $getString option is requested
* @since 3.0.106
*
*/
public function getTags($getString = false) {
if($this->tagList === null) {
$tagList = $this->setTags(parent::get('tags'));
} else {
$tagList = $this->tagList;
}
if($getString !== false) {
$delimiter = $getString === true ? ' ' : $getString;
return implode($delimiter, $tagList);
}
return $tagList;
}
/**
* Set all tags
*
* #pw-internal
*
* @param array $tagList Array of tags to add
* @param bool $reindex Set to false to set given $tagsList exactly as-is (assumes it's already in correct format)
* @return array Array of tags that were set
* @since 3.0.106
*
*/
public function setTags($tagList, $reindex = true) {
if($tagList === null || $tagList === '') {
$tagList = array();
} else if(!is_array($tagList)) {
$tagList = explode(' ', $tagList);
}
if($reindex && count($tagList)) {
$tags = array();
foreach($tagList as $tag) {
$tag = trim($tag);
if(strlen($tag)) $tags[strtolower($tag)] = $tag;
}
$tagList = $tags;
}
if($this->tagList !== $tagList) {
$this->tagList = $tagList;
parent::set('tags', implode(' ', $tagList));
$this->wire('fields')->getTags('reset');
}
return $tagList;
}
/**
* Add one or more tags
*
* @param string $tag
* @return array Returns current tag list
* @since 3.0.106
*
*/
public function addTag($tag) {
$tagList = $this->getTags();
$tagList[strtolower($tag)] = $tag;
$this->setTags($tagList, false);
return $tagList;
}
/**
* Return true if this field has the given tag or false if not
*
* @param string $tag
* @return bool
* @since 3.0.106
*
*/
public function hasTag($tag) {
$tagList = $this->getTags();
return isset($tagList[strtolower(trim(ltrim($tag, '-')))]);
}
/**
* Remove a tag
*
* @param string $tag
* @return array Returns current tag list
* @since 3.0.106
*
*/
public function removeTag($tag) {
$tagList = $this->getTags();
$tag = strtolower($tag);
if(!isset($tagList[$tag])) return $tagList;
unset($tagList[$tag]);
return $this->setTags($tagList, false);
}
/**
* debugInfo PHP 5.6+ magic method
*

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

@@ -5,7 +5,7 @@
*
* Manages collection of ALL Field instances, not specific to any particular Fieldgroup
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
* #pw-summary Manages all custom fields in ProcessWire
@@ -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.
*
*/
@@ -36,6 +37,8 @@ class Fields extends WireSaveableItems {
static protected $nativeNamesSystem = array(
'child',
'children',
'count',
'check_access',
'created_users_id',
'created',
'createdUser',
@@ -43,11 +46,16 @@ class Fields extends WireSaveableItems {
'createdUsersID',
'data',
'description',
'editUrl',
'end',
'fieldgroup',
'fields',
'find',
'flags',
'get',
'has_parent',
'hasParent',
'httpUrl',
'id',
'include',
'isNew',
@@ -75,20 +83,33 @@ class Fields extends WireSaveableItems {
'templatePrevious',
'templates_id',
'url',
'_custom',
);
/**
* Field names that are native/permanent to this instance of ProcessWire (configurable at runtime)
*
* Array indexes are the names and values are all boolean true.
*
*/
protected $nativeNamesLocal = array();
/**
* Cache of all tags for all fields, populated to array when asked for the first time
*
* @var array|null
*
*/
protected $tagList = null;
/**
* Construct
*
*/
public function __construct() {
$this->fieldsArray = new FieldsArray();
// convert so that keys are names so that isset() can be used rather than in_array()
if(isset(self::$nativeNamesSystem[0])) self::$nativeNamesSystem = array_flip(self::$nativeNamesSystem);
}
/**
@@ -206,6 +227,8 @@ class Fields extends WireSaveableItems {
}
}
}
$this->getTags('reset');
return true;
}
@@ -282,7 +305,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
@@ -623,14 +646,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
);
}
@@ -677,6 +700,8 @@ class Fields extends WireSaveableItems {
'countPages' => false,
'getPageIDs' => false,
);
if(!$field->type) return 0;
$options = array_merge($defaults, $options);
$database = $this->wire('database');
@@ -803,8 +828,8 @@ class Fields extends WireSaveableItems {
*
*/
public function isNative($name) {
if(in_array($name, self::$nativeNamesSystem)) return true;
if(in_array($name, $this->nativeNamesLocal)) return true;
if(isset(self::$nativeNamesSystem[$name])) return true;
if(isset($this->nativeNamesLocal[$name])) return true;
return false;
}
@@ -817,7 +842,76 @@ class Fields extends WireSaveableItems {
*
*/
public function setNative($name) {
$this->nativeNamesLocal[] = $name;
$this->nativeNamesLocal[$name] = true;
}
/**
* Get list of all tags used by fields
*
* - By default it returns an array of tag names where both keys and values are the tag names.
* - If you specify true for the `$getFields` argument, it returns an array where the keys are
* tag names and the values are arrays of field names in the tag.
* - If you specify "reset" for the `$getFields` argument it returns a blank array and resets
* internal tags cache.
*
* @param bool|string $getFieldNames Specify true to return associative array where keys are tags and values are field names
* …or specify the string "reset" to force getTags() to reset its cache, forcing it to reload on the next call.
* @return array
* @since 3.0.106
*
*/
public function getTags($getFieldNames = false) {
if($getFieldNames === 'reset') {
$this->tagList = null;
return array();
}
if($this->tagList === null) {
$tagList = array();
foreach($this as $field) {
/** @var Field $field */
$fieldTags = $field->getTags();
foreach($fieldTags as $tag) {
if(!isset($tagList[$tag])) $tagList[$tag] = array();
$tagList[$tag][] = $field->name;
}
}
ksort($tagList);
$this->tagList = $tagList;
}
if($getFieldNames) return $this->tagList;
$tagList = array();
foreach($this->tagList as $tag => $fieldNames) {
$tagList[$tag] = $tag;
}
return $tagList;
}
/**
* Return all fields that have the given $tag
*
* Returns an associative array of `['field_name' => 'field_name']` if `$getFieldNames` argument is true,
* or `['field_name => Field instance]` if not (which is the default).
*
* @param string $tag Tag to find fields for
* @param bool $getFieldNames If true, returns array of field names rather than Field objects (default=false).
* @return array Array of Field objects, or array of field names if requested. Array keys are always field names.
* @since 3.0.106
*
*/
public function findByTag($tag, $getFieldNames = false) {
$tags = $this->getTags(true);
$items = array();
if(!isset($tags[$tag])) return $items;
foreach($tags[$tag] as $fieldName) {
$items[$fieldName] = ($getFieldNames ? $fieldName : $this->get($fieldName));
}
ksort($items);
return $items;
}
/**

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)
@@ -76,6 +77,14 @@ abstract class Fieldtype extends WireData implements Module {
*/
protected $loadPageFieldFilters = null;
/**
* Field that last referenced this Fieldtype from $field->type
*
* @var Field|null
*
*/
protected $lastAccessField = null;
/**
* Construct
*
@@ -90,6 +99,30 @@ abstract class Fieldtype extends WireData implements Module {
*/
public function init() { }
/**
* Set last access field
*
* #pw-internal
*
* @param Field $field
*
*/
public function setLastAccessField(Field $field) {
$this->lastAccessField = $field;
}
/**
* Return field that last accessed this Fieldtype via $field->type
*
* #pw-internal
*
* @return null|Field
*
*/
public function getLastAccessField() {
return $this->lastAccessField;
}
/**
* Fieldtype modules are singular, in that only one instance is needed per request
*
@@ -521,6 +554,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) {
@@ -533,12 +567,15 @@ abstract class Fieldtype extends WireData implements Module {
*
* 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|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, array $options = array()) {
@@ -547,6 +584,43 @@ abstract class Fieldtype extends WireData implements Module {
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
*
@@ -1267,7 +1341,7 @@ abstract class Fieldtype extends WireData implements Module {
if($key == 'name') return $this->className();
if($key == 'shortName') {
return str_replace('Fieldtype', '', $this->className());
} else if($key == 'longName') {
} else if($key == 'longName' && method_exists($this, 'getModuleInfo')) {
$info = $this->getModuleInfo($this);
return $info['title'];
}

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

@@ -388,7 +388,7 @@ class FileCompiler extends Wire {
$targetData = $this->compileData($targetData, $sourcePathname);
if(false !== file_put_contents($targetPathname, $targetData, LOCK_EX)) {
$this->chmod($targetPathname);
touch($targetPathname, filemtime($sourcePathname));
$this->touch($targetPathname, filemtime($sourcePathname));
$targetHash = md5_file($targetPathname);
$cacheData = array(
'source' => array(
@@ -977,7 +977,7 @@ class FileCompiler extends Wire {
copy($sourceFile, $targetFile);
$this->chmod($targetFile);
touch($targetFile, filemtime($sourceFile));
$this->touch($targetFile, filemtime($sourceFile));
$numCopied++;
}
@@ -1060,7 +1060,7 @@ class FileCompiler extends Wire {
// maintenance already run today
return false;
}
touch($lastRunFile);
$this->touch($lastRunFile);
$this->chmod($lastRunFile);
clearstatcache();
@@ -1110,14 +1110,14 @@ class FileCompiler extends Wire {
if(!file_exists($sourceFile)) {
// source file has been deleted
unlink($targetFile);
$this->wire('files')->unlink($targetFile, true);
if($useLog) $this->log("Maintenance/Remove target file: $targetURL$basename");
} else if(filemtime($sourceFile) != filemtime($targetFile)) {
} else if(filemtime($sourceFile) > filemtime($targetFile)) {
// source file has changed
copy($sourceFile, $targetFile);
$this->chmod($targetFile);
touch($targetFile, filemtime($sourceFile));
$this->touch($targetFile, filemtime($sourceFile));
if($useLog) $this->log("Maintenance/Copy new version of source file to target file: $sourceURL$basename => $targetURL$basename");
}
}
@@ -1162,5 +1162,29 @@ class FileCompiler extends Wire {
$this->exclusions[] = $pathname;
}
/**
* Same as PHP touch() but with fallbacks for cases where touch() does not work
*
* @param string $filename
* @param null|int $time
* @return bool
*
*/
protected function touch($filename, $time = null) {
if($time === null) {
$result = @touch($filename);
} else {
$result = @touch($filename, $time);
// try again, but without time
if(!$result) $result = @touch($filename);
}
if(!$result) {
// lastly try alternative method which should have same affect as touch without $time
$fp = fopen($filename, 'a');
$result = $fp !== false ? fclose($fp) : false;
}
return $result;
}
}

View File

@@ -451,11 +451,11 @@ class FileLog extends Wire {
fclose($fpr);
if($cnt) {
unlink($filename);
rename("$filename.new", $filename);
$this->wire('files')->unlink($filename, true);
$this->wire('files')->rename("$filename.new", $filename, true);
$this->wire('files')->chmod($filename);
} else {
@unlink("$filename.new");
$this->wire('files')->unlink("$filename.new", true);
}
return $cnt;
@@ -477,8 +477,8 @@ class FileLog extends Wire {
'dateTo' => time(),
));
if(file_exists($toFile)) {
unlink($this->logFilename);
rename($toFile, $this->logFilename);
$this->wire('files')->unlink($this->logFilename, true);
$this->wire('files')->rename($toFile, $this->logFilename, true);
return $qty;
}
return 0;
@@ -491,7 +491,7 @@ class FileLog extends Wire {
*
*/
public function delete() {
return @unlink($this->logFilename);
return $this->wire('files')->unlink($this->logFilename, true);
}
public function __toString() {

View File

@@ -501,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)
@@ -627,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;
}
/**
@@ -758,6 +840,82 @@ function wireClassParents($className, $autoload = true) {
return $a;
}
/**
* Does given instance (or class) represent an instance of the given className (or class names)?
*
* Since version 3.0.108 the $className argument may also represent an interface,
* array of interfaces, or mixed array of interfaces and class names. Previous versions did
* not support interfaces unless the $instance argument was an object.
*
* @param object|string $instance Object instance to test (or string of its class name).
* @param string|array $className Class/interface name or array of class/interface 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/interface 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;
$instanceIsObject = is_object($instance);
$instanceParents = null;
$instanceInterfaces = null;
$instanceClass = null;
if($instanceIsObject) {
// instance is an object
} else if(is_string($instance)) {
// instance is a class name, make sure it has namespace
$instanceClass = wireClassName($instance, true);
if($instanceClass === null) $instanceClass = $instance; // if above failed
$instance = $instanceClass;
} else {
// unrecognized instance value
return false;
}
foreach($classNames as $className) {
$className = wireClassName($className, true); // with namespace
if($instanceIsObject && (class_exists($className, $autoload) || interface_exists($className, $autoload))) {
if($instance instanceof $className) {
$matchClass = $className;
}
} else {
if($instanceClass === null) {
$instanceClass = wireClassName($instance, true);
if($instanceClass === null) break;
}
if($instanceParents === null) {
$instanceParents = wireClassParents($instance, $autoload);
$instanceParents[$instanceClass] = 1;
}
if(isset($instanceParents[$className])) {
$matchClass = $className;
} else {
if($instanceInterfaces === null) {
$instanceInterfaces = wireClassImplements($instance, $autoload);
}
if(isset($instanceInterfaces[$className])) {
$matchClass = $className;
}
}
}
if($matchClass !== null) break;
}
return $returnClass ? $matchClass : ($matchClass !== null);
}
/**
* ProcessWire namespace aware version of PHP's is_callable() function
*
@@ -772,6 +930,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)
*
@@ -854,3 +1031,26 @@ function wireRegion($key, $value = null) {
return $result;
}
/**
* Create new WireArray, add given $items to it, and return it
*
* @param array|WireArray $items
* @return WireArray
*
*/
function WireArray($items = array()) {
return WireArray::newInstance($items);
}
/**
* Create new PageArray, add given $items (pages) to it, and return it
*
* @param array|PageArray $items
* @return WireArray
*
*/
function PageArray($items = array()) {
return PageArray::newInstance($items);
}

View File

@@ -430,3 +430,47 @@ function region($key = '', $value = null) {
return wireRegion($key, $value);
}
/**
* Get or set a runtime site setting
*
* This is a simple helper function for maintaining runtime settings in a site profile.
* It simply sets and gets settings that you define. It is preferable to using ProcessWires
* `$config` or `config()` API var/function because it is not used to store anything else for
* ProcessWire. It is also preferable to using a variable (or variables) because it is always
* in scope and accessible anywhere in your template files, even within existing functions.
*
* ~~~~~
* // set a setting named “foo” to value “bar”
* setting('foo', 'bar');
*
* // get a setting named “foo”
* $value = setting('foo');
*
* // set or replace multiple settings
* setting([
* 'foo' => 'value',
* 'bar' => 123,
* 'baz' => [ 'foo', 'bar', 'baz' ]
* ]);
*
* // get all settings in associative array
* $a = setting();
*
* // to unset a setting
* setting(false, 'foo');
* ~~~~~
*
* @param string|array $name Setting name, or array to set multiple
* @param string|int|array|float|mixed $value Value to set, or omit if getting value of $name (default=null)
* @return array|string|int|bool|mixed|null
*
*/
function setting($name = '', $value = null) {
static $settings = [];
if($name === '') return $settings;
if(is_array($name)) return $settings = array_merge($settings, $name);
if($name === false) { unset($settings[(string) $value]); return null; }
if($value !== null) $settings[$name] = $value;
return isset($settings[$name]) ? $settings[$name] : null;
}

View File

@@ -135,7 +135,7 @@ class ImageInspector extends WireData {
if(is_array($additionalInfo) && $parseAppmarker) {
$appmarker = array();
foreach($additionalInfo as $k => $v) {
$appmarker[$k] = substr($v, 0, strpos($v, null));
$appmarker[$k] = substr($v, 0, strpos($v, chr(null)));
}
$this->info['appmarker'] = $appmarker;
if(isset($additionalInfo['APP13'])) {
@@ -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
*
@@ -510,9 +552,9 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
$dest = preg_replace('/\.' . $extension . '$/', '_tmp.' . $extension, $filename);
if(strlen($content) == @file_put_contents($dest, $content, \LOCK_EX)) {
// on success we replace the file
unlink($filename);
rename($dest, $filename);
wireChmod($filename);
$this->wire('files')->unlink($filename);
$this->wire('files')->rename($dest, $filename);
$this->wire('files')->chmod($filename);
return true;
} else {
// it was created a temp diskfile but not with all data in it
@@ -627,28 +669,28 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
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
while($pWidth > $img['width'] || $pHeight > $img['height']) {
// favor the smallest dimension
if($pWidth > $img['width']) {
$pWidth = $img['width'];
$pHeight = $this->getProportionalHeight($pWidth);
}
if($pHeight > $img['height']) {
$pHeight = $img['height'];
$pWidth = $this->getProportionalWidth($pHeight);
}
if($targetWidth > $pWidth) $targetWidth = $pWidth;
if($targetHeight > $pHeight) $targetHeight = $pHeight;
if(!$this->cropping) {
$targetWidth = $pWidth;
$targetHeight = $pHeight;
}
if(!$this->upscaling && ($img['width'] < $targetWidth || $img['height'] < $targetHeight)) {
// via @horst-n PR #118:
// upscaling is not allowed and we have one or both dimensions to small,
// we scale down the target dimensions to fit within the image dimensions,
// with respect to the target dimensions ratio
$ratioSource = $img['height'] / $img['width'];
$ratioTarget = !$this->cropping ? $ratioSource : $targetHeight / $targetWidth;
if($ratioSource >= $ratioTarget) {
// ratio is equal or target fits into source
$pWidth = $targetWidth = $img['width'];
$pHeight = $img['height'];
$targetHeight = ceil($pWidth * $ratioTarget);
} else {
// target doesn't fit into source
$pHeight = $targetHeight = $img['height'];
$pWidth = $img['width'];
$targetWidth = ceil($pHeight / $ratioTarget);
}
if($this->cropping) {
// we have to disable any sharpening method here,
// as the source will not be resized, only cropped
$this->sharpening = 'none';
}
}
@@ -703,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]);
}
}
@@ -736,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
@@ -794,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);
@@ -910,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
@@ -1059,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;
@@ -1126,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,
@@ -1327,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;
@@ -1361,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;
}
}
}
@@ -1431,8 +1540,8 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
// all went well, copy back the temp file, remove the temp file
if(!@copy($this->tmpFile, $this->filename)) return false; // fallback or failed
wireChmod($this->filename);
@unlink($this->tmpFile);
$this->wire('files')->chmod($this->filename);
$this->wire('files')->unlink($this->tmpFile);
// post processing: IPTC, setModified and reload ImageInfo
$this->writeBackIPTC($this->filename, false);
@@ -1442,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)) $this->wire('files')->unlink($dstFilename);
$this->wire('files')->rename($tmpFilename, $dstFilename);
}
$this->wire('files')->chmod($dstFilename);
} else {
// fail
if(is_file($tmpFilename)) $this->wire('files')->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
*
@@ -1475,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
@@ -224,17 +246,33 @@ class ImageSizerEngineGD extends ImageSizerEngine {
$this->prepareImageLayer($thumb, $image);
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $finalWidth, $finalHeight, $this->image['width'], $this->image['height']);
} else if(4 == $resizeMethod) { // 4 = resize and crop with aspect ratio
} else if(4 == $resizeMethod) { // 4 = resize and crop with aspect ratio, - or crop without resizing ($upscaling == false)
// we have to scale up or down and to _crop_
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'];
$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 +280,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 +322,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 +357,6 @@ class ImageSizerEngineGD extends ImageSizerEngine {
return $result;
}
/**
* Rotate image (@horst)
*
@@ -313,7 +370,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 +455,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 +599,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 +623,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 +661,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 +779,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 +814,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 +828,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 +840,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

@@ -45,16 +45,30 @@
* @property mixed $value HTML 'value' attribute for the Inputfield. #pw-group-attribute-properties
* @property string $class HTML 'class' attribute for the Inputfield. #pw-group-attribute-properties
*
* @method string|Inputfield name($name = null) Get or set the name attribute. @since 3.0.110 #pw-group-attribute-methods
* @method string|Inputfield id($id = null) Get or set the id attribute. @since 3.0.110 #pw-group-attribute-methods
* @method string|Inputfield class($class = null) Get class attribute or add a class to the class attribute. @since 3.0.110 #pw-group-attribute-methods
*
* LABELS & CONTENT
* ================
* @property string $label Primary label text that appears above the input. #pw-group-labels
* @property string $description Optional description that appears under label to provide more detailed information. #pw-group-labels
* @property string $notes Optional notes that appear under input area to provide additional notes. #pw-group-labels
* @property string $icon Optional font-awesome icon name to accompany label (excluding the "fa-") part). #pw-group-labels
* @property string $requiredLabel Optional custom label to display when missing required value. @since 3.0.98 #pw-group-labels
* @property string $head Optional text that appears below label but above description (only used by some Inputfields). #pw-internal
* @property string|null $prependMarkup Optional markup to prepend to the Inputfield content container. #pw-group-other
* @property string|null $appendMarkup Optional markup to append to the Inputfield content container. #pw-group-other
*
* @method string|Inputfield label($label = null) Get or set the 'label' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield description($description = null) Get or set the 'description' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield notes($notes = null) Get or set the 'notes' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield icon($icon = null) Get or set the 'icon' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield requiredLabel($requiredLabel = null) Get or set the 'requiredLabel' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield head($head = null) Get or set the 'head' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield prependMarkup($markup = null) Get or set the 'prependMarkup' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield appendMarkup($markup = null) Get or set the 'appendMarkup' property via method. @since 3.0.110 #pw-group-labels
*
* APPEARANCE
* ==========
* @property int $collapsed Whether the field is collapsed or visible, using one of the "collapsed" constants. #pw-group-appearance
@@ -62,13 +76,20 @@
* @property int $columnWidth Width of column for this Inputfield 10-100 percent. 0 is assumed to be 100 (default). #pw-group-appearance
* @property int $skipLabel Skip display of the label? See the "skipLabel" constants for options. #pw-group-appearance
*
* @method int|Inputfield collapsed($collapsed = null) Get or set collapsed property via method. @since 3.0.110 #pw-group-appearance
* @method string|Inputfield showIf($showIf = null) Get or set showIf selector property via method. @since 3.0.110 #pw-group-appearance
* @method int|Inputfield columnWidth($columnWidth = null) Get or set columnWidth property via method. @since 3.0.110 #pw-group-appearance
* @method int|Inputfield skipLabel($skipLabel = null) Get or set the skipLabel constant property via method. @since 3.0.110 #pw-group-appearance
*
*
* SETTINGS & BEHAVIOR
* ===================
* @property int|bool $required Set to true (or 1) to make input required, or false (or 0) to make not required (default=0). #pw-group-behavior
* @property string $requiredIf Optional conditions under which input is required (selector string). #pw-group-behavior
* @property 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 or null when not applicable or 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|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
@@ -76,6 +97,14 @@
* @property string $wrapClass Optional class name (CSS) to apply to the HTML element wrapping the Inputfield. #pw-group-other
* @property string $headerClass Optional class name (CSS) to apply to the InputfieldHeader element #pw-group-other
* @property string $contentClass Optional class name (CSS) to apply to the InputfieldContent element #pw-group-other
*
* @method string|Inputfield required($required = null) Get or set required state. @since 3.0.110 #pw-group-behavior
* @method string|Inputfield requiredIf($requiredIf = null) Get or set required-if selector. @since 3.0.110 #pw-group-behavior
*
* @method string|Inputfield wrapClass($class = null) Get wrapper class attribute or add a class to it. @since 3.0.110 #pw-group-other
* @method string|Inputfield headerClass($class = null) Get header class attribute or add a class to it. @since 3.0.110 #pw-group-other
* @method string|Inputfield contentClass($class = null) Get content class attribute or add a class to it. @since 3.0.110 #pw-group-other
*
*
* HOOKABLE METHODS
* ================
@@ -260,7 +289,7 @@ abstract class Inputfield extends WireData implements Module {
*/
/**
* Attributes specified for the XHTML output, like class, rows, cols, etc.
* Attributes specified for the HTML output, like class, rows, cols, etc.
*
*/
protected $attributes = array();
@@ -303,7 +332,7 @@ abstract class Inputfield extends WireData implements Module {
self::$numInstances++;
$this->set('label', ''); // primary clikable label
$this->set('label', ''); // primary clickable label
$this->set('description', ''); // descriptive copy, below label
$this->set('icon', ''); // optional icon name to accompany label
$this->set('notes', ''); // highlighted descriptive copy, below output of input field
@@ -319,6 +348,8 @@ abstract class Inputfield extends WireData implements Module {
$this->set('contentClass', ''); // optional class to apply to InputfieldContent wrapper
$this->set('textFormat', self::textFormatBasic); // format applied to description and notes
$this->set('renderValueFlags', 0); // see renderValue* constants, applicable to renderValue mode only
$this->set('prependMarkup', '');
$this->set('appendMarkup', '');
// default ID attribute if no 'id' attribute set
$this->defaultID = $this->className() . self::$numInstances;
@@ -465,6 +496,18 @@ abstract class Inputfield extends WireData implements Module {
return $this;
}
/**
* Unset any previously set parent
*
* #pw-internal
* @return $this
*
*/
public function unsetParent() {
$this->parent = null;
return $this;
}
/**
* Get this Inputfields parent InputfieldWrapper, or NULL if it doesnt have one.
*
@@ -495,6 +538,80 @@ abstract class Inputfield extends WireData implements Module {
return $parents;
}
/**
* Get or set parent of Inputfield
*
* This convenience method performs the same thing as getParent() and setParent().
*
* To get parent, specify no arguments. It will return null if no parent assigned, or an
* InputfieldWrapper instance of the parent.
*
* To set parent, specify an InputfieldWrapper for the $parent argument. The return value
* is the current Inputfield for fluent interface.
*
* #pw-group-traversal
*
* @param null|InputfieldWrapper $parent
* @return null|Inputfield|InputfieldWrapper
* @since 3.0.110
*
*/
public function parent($parent = null) {
if($parent === null) {
return $this->getParent();
} else {
return $this->setParent($parent);
}
}
/**
* Get array of all parents of this Inputfield
*
* This is identical to and an alias of the getParents() method.
*
* #pw-group-traversal
*
* @return array
* @since 3.0.110
*
*/
public function parents() {
return $this->getParents();
}
/**
* Get the root parent InputfieldWrapper element (farthest parent, commonly InputfieldForm)
*
* This returns null only if Inputfield it is called from has not yet been added to an InputfieldWrapper.
*
* #pw-group-traversal
*
* @return InputfieldForm|InputfieldWrapper|null
* @since 3.0.106
*
*/
public function getRootParent() {
$parents = $this->getParents();
return count($parents) ? end($parents) : null;
}
/**
* Get the InputfieldForm element that contains this field or null if not yet defined
*
* This is the same as the `getRootParent()` method except that it returns null if root parent
* is not an InputfieldForm.
*
* #pw-group-traversal
*
* @return InputfieldForm|null
* @since 3.0.106
*
*/
public function getForm() {
$form = $this instanceof InputfieldForm ? $this : $this->getRootParent();
return ($form instanceof InputfieldForm ? $form : null);
}
/**
* Set an attribute
*
@@ -680,6 +797,39 @@ abstract class Inputfield extends WireData implements Module {
return $this->setAttribute('value', $value);
}
/**
* If method call resulted in no handler, this hookable method is called.
*
* We use this to allow for attributes and properties to be set via method, useful primarily
* for fluent interface calls.
*
* #pw-internal
*
* @param string $method Requested method name
* @param array $arguments Arguments provided
* @return null|mixed Return value of method (if applicable)
* @throws WireException
* @since 3.0.110
*
*/
protected function ___callUnknown($method, $arguments) {
$arg = isset($arguments[0]) ? $arguments[0] : null;
if(isset($this->attributes[$method])) {
// get or set an attribute
return $arg === null ? $this->getAttribute($method) : $this->setAttribute($method, $arg);
} else if(($value = $this->getSetting($method)) !== null) {
// get or set a setting
if($arg === null) return $value;
if(stripos($method, 'class') !== false) {
// i.e. class, wrapClass, contentClass, etc.
return $this->addClass($arg, $method);
} else {
return $this->set($method, $arg);
}
}
return parent::___callUnknown($method, $arguments);
}
/**
* Get all attributes specified for this Inputfield
*
@@ -1068,7 +1218,7 @@ abstract class Inputfield extends WireData implements Module {
* @param bool $renderValueMode
*
*/
public function ___renderReadyHook(Inputfield $parent = null, $renderValueMode) { }
public function ___renderReadyHook(Inputfield $parent = null, $renderValueMode = false) { }
/**
* This hook was replaced by renderReady
@@ -1456,7 +1606,9 @@ abstract class Inputfield extends WireData implements Module {
$errors[] = $text;
$this->wire('session')->set($key, $errors);
}
$text .= $this->name ? " ($this->name)" : "";
$label = $this->getSetting('label');
if(empty($label)) $label= $this->attr('name');
if(strlen($label)) $text .= " - $label";
return parent::error($text, $flags);
}
@@ -1567,9 +1719,12 @@ abstract class Inputfield extends WireData implements Module {
*/
public function entityEncode($str, $markdown = false) {
/** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
// if already encoded, then un-encode it
if(strpos($str, '&') !== false && preg_match('/&(#\d+|[a-zA-Z]+);/', $str)) {
$str = html_entity_decode($str, ENT_QUOTES, "UTF-8");
$str = $sanitizer->unentities($str);
}
if($markdown && $markdown !== self::textFormatNone) {
@@ -1581,17 +1736,17 @@ abstract class Inputfield extends WireData implements Module {
if(!$textFormat) $textFormat = self::textFormatBasic;
if($textFormat & self::textFormatBasic) {
// only basic markdown allowed (default behavior)
$str = $this->wire('sanitizer')->entitiesMarkdown($str, array('allowBrackets' => true));
$str = $sanitizer->entitiesMarkdown($str, array('allowBrackets' => true));
} else if($textFormat & self::textFormatMarkdown) {
// full markdown, plus HTML is also allowed
$str = $this->wire('sanitizer')->entitiesMarkdown($str, array('fullMarkdown' => true));
$str = $sanitizer->entitiesMarkdown($str, array('fullMarkdown' => true));
} else {
// nothing allowed, text fully entity encoded regardless of $markdown request
$str = $this->wire('sanitizer')->entities($str);
$str = $sanitizer->entities($str);
}
} else {
$str = $this->wire('sanitizer')->entities($str);
$str = $sanitizer->entities($str);
}
return $str;

View File

@@ -24,6 +24,7 @@
* @property InputfieldsArray|null $children Inputfield instances that are direct children of this InputfieldWrapper. #pw-group-properties
*
* @method string renderInputfield(Inputfield $inputfield, $renderValueMode = false) #pw-group-output
* @method Inputfield new($typeName, $name = '', $label = '', array $settings = array()) #pw-group-manipulation
*
*/
@@ -185,23 +186,87 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
/**
* Add an Inputfield item as a child (also accepts array definition)
*
* Since 3.0.110: If given a string value, it is assumed to be an Inputfield type that you
* want to add. In that case, it will create the Inputfield and return it instead of $this.
*
* #pw-group-manipulation
*
* @param Inputfield|array $item
* @return $this
* @param Inputfield|array|string $item
* @return Inputfield|InputfieldWrapper|$this
* @see InputfieldWrapper::import()
*
*/
public function add($item) {
if(is_array($item)) {
if(is_string($item)) {
return $this->___new($item);
} else if(is_array($item)) {
$this->importArray($item);
} else {
$item->setParent($this);
$this->children->add($item);
$item->setParent($this);
}
return $this;
}
/**
* Create a new Inputfield, add it to this InputfieldWrapper, and return the new Inputfield
*
* - Only the $typeName argument is required.
* - You may optionally substitute the $settings argument for the $name or $label arguments.
* - You may optionally substitute Inputfield “description” property for $settings argument.
*
* #pw-group-manipulation
*
* @param string $typeName Inputfield type, i.e. “InputfieldCheckbox” or just “checkbox” for short.
* @param string|array $name Name of input (or substitute $settings here).
* @param string|array $label Label for input (or substitute $settings here).
* @param array|string $settings Settings to add to Inputfield (optional). Or if string, assumed to be “description”.
* @return Inputfield|InputfieldSelect|InputfieldWrapper An Inputfield instance ready to populate with additional properties/attributes.
* @throws WireException If you request an unknown Inputfield type
* @since 3.0.110
*
*/
public function ___new($typeName, $name = '', $label = '', $settings = array()) {
if(is_array($name)) {
$settings = $name;
$name = '';
} else if(is_array($label)) {
$settings = $label;
$label = '';
}
if(strpos($typeName, 'Inputfield') !== 0) {
$typeName = "Inputfield" . ucfirst($typeName);
}
/** @var Inputfield|InputfieldSelect|InputfieldWrapper $inputfield */
$inputfield = $this->wire('modules')->getModule($typeName);
if(!$inputfield && wireClassExists($typeName)) {
$inputfield = $this->wire(new $typeName());
}
if(!$inputfield || !$inputfield instanceof Inputfield) {
throw new WireException("Unknown Inputfield type: $typeName");
}
if(strlen($name)) $inputfield->attr('name', $name);
if(strlen($label)) $inputfield->label = $label;
if(is_array($settings)) {
foreach($settings as $key => $value) {
$inputfield->set($key, $value);
}
} else if(is_string($settings)) {
$inputfield->description = $settings;
}
$this->add($inputfield);
return $inputfield;
}
/**
* Import the given Inputfield items as children
*
@@ -351,6 +416,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
array_push($wrappers, $inputfield);
}
$inputfield->unsetParent();
$wrapper->add($inputfield);
}
@@ -381,7 +447,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
$useColumnWidth = $this->useColumnWidth;
$renderAjaxInputfield = $this->wire('config')->ajax ? $this->wire('input')->get('renderInputfieldAjax') : null;
if(isset($_classes['form']) && strpos($_classes['form'], 'InputfieldFormNoWidths') !== false) {
if($useColumnWidth === true && isset($_classes['form']) && strpos($_classes['form'], 'InputfieldFormNoWidths') !== false) {
$useColumnWidth = false;
}
@@ -434,8 +500,12 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
foreach(array('error', 'description', 'head', 'notes') as $property) {
$text = $property == 'error' ? $errorsOut : $inputfield->getSetting($property);
if(!empty($text) && !$quietMode) {
$text = nl2br($entityEncodeText ? $inputfield->entityEncode($text, true) : $text);
$text = str_replace('{out}', $text, $markup["item_$property"]);
if($entityEncodeText) {
$text = $inputfield->entityEncode($text, true);
}
if($inputfield->textFormat != Inputfield::textFormatMarkdown) {
$text = str_replace('{out}', nl2br($text), $markup["item_$property"]);
}
} else {
$text = '';
}
@@ -480,7 +550,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
$ffAttrs['class'] .= ' ' . $classes['item_required_if'];
}
if($collapsed) {
if($collapsed && $collapsed !== Inputfield::collapsedNever) {
$isEmpty = $inputfield->isEmpty();
if(($isEmpty && $inputfield instanceof InputfieldWrapper) ||
$collapsed === Inputfield::collapsedYes ||
@@ -548,17 +618,28 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
// no header
// $inputfield->addClass('InputfieldNoHeader', 'wrapClass');
}
$columnWidth = (int) $inputfield->getSetting('columnWidth');
$columnWidthAdjusted = $columnWidth + ($columnWidthTotal ? -1 * $columnWidthSpacing : 0);
$columnWidthAdjusted = $columnWidth;
if($columnWidthSpacing) {
$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%;";
} else {
$ffAttrs['data-colwidth'] = "$columnWidthAdjusted%";
if(!$columnWidthTotal) {
$ffAttrs['class'] .= ' ' . $classes['item_column_width_first'];
}
$columnWidthTotal += $columnWidth;
if(!$useColumnWidth || $useColumnWidth > 1) {
if($columnWidthTotal >= 95 && $columnWidthTotal < 100) {
$columnWidthAdjusted += (100 - $columnWidthTotal);
$columnWidthTotal = 100;
}
$ffAttrs['data-colwidth'] = "$columnWidthAdjusted%";
}
if($useColumnWidth) {
$ffAttrs['style'] = "width: $columnWidthAdjusted%;";
}
//if($columnWidthTotal >= 100 && !$requiredIf) $columnWidthTotal = 0; // requiredIf meant to be a showIf?
if($columnWidthTotal >= 100) $columnWidthTotal = 0;
} else {
@@ -709,23 +790,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);
}
}
@@ -742,6 +808,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
*
@@ -771,7 +876,9 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
// check if a value is required and field is empty, trigger an error if so
if($child->name && $child->getSetting('required') && $child->isEmpty()) {
$child->error($this->requiredLabel);
$requiredLabel = $child->getSetting('requiredLabel');
if(empty($requiredLabel)) $requiredLabel = $this->requiredLabel;
$child->error($requiredLabel);
}
}
@@ -859,6 +966,34 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
return $empty;
}
/**
* Return Inputfields in this wrapper that are required and have empty values
*
* This method includes all children up through the tree, not just direct children.
*
* #pw-internal
*
* @param bool $required Only include empty Inputfields that are required? (default=true)
* @return array of Inputfield instances indexed by name attributes
*
*/
public function getEmpty($required = true) {
$a = array();
static $n = 0;
foreach($this->children as $child) {
if($child instanceof InputfieldWrapper) {
$a = array_merge($a, $child->getEmpty($required));
} else {
if($required && !$child->getSetting('required')) continue;
if(!$child->isEmpty()) continue;
$name = $child->attr('name');
if(empty($name)) $name = "_unknown" . (++$n);
$a[$name] = $child;
}
}
return $a;
}
/**
* Return an array of errors that occurred on any of the children during input processing.
*
@@ -900,6 +1035,61 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
}
}
/**
* Find an Inputfield below this one that has the given name
*
* This is an alternative to the `getChildByName()` method, with more options for when you need it.
* For instance, it can also accept a selector string or numeric index for the $name argument, and you
* can optionally disable the $recursive behavior.
*
* #pw-group-retrieval-and-traversal
*
* @param string|int $name Name or selector string of child to find, omit for first child, or specify zero-based index of child to return.
* @param bool $recursive Find child recursively? Looks for child in this wrapper, and all other wrappers below it. (default=true)
* @return Inputfield|null Returns Inputfield instance if found, or null if not.
* @since 3.0.110
*
*/
public function child($name = '', $recursive = true) {
$child = null;
if(!$this->children->count()) {
// no child possible
} else if(empty($name)) {
// first child
$child = $this->children->first();
} else if(is_int($name)) {
// number index
$child = $this->children->eq($name);
} else if($this->wire('sanitizer')->name($name) === $name) {
// child by name
$wrappers = array();
foreach($this->children as $f) {
if($f->getAttribute('name') === $name) {
$child = $f;
break;
} else if($recursive && $f instanceof InputfieldWrapper) {
$wrappers[] = $f;
}
}
if(!$child && $recursive && count($wrappers)) {
foreach($wrappers as $wrapper) {
$child = $wrapper->child($name, $recursive);
if($child) break;
}
}
} else if(Selectors::stringHasSelector($name)) {
// first child matching selector string
$child = $this->children("$name, limit=1")->first();
}
return $child;
}
/**
* Return all children Inputfields (alias of children method)
*
@@ -1031,7 +1221,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
* #pw-internal
*
* @param bool $trackChanges
* @return $this
* @return Inputfield|InputfieldWrapper
*
*/
public function setTrackChanges($trackChanges = true) {
@@ -1045,7 +1235,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
* #pw-internal
*
* @param bool $trackChanges
* @return $this
* @return Inputfield|InputfieldWrapper
*
*/
public function resetTrackChanges($trackChanges = true) {

View File

@@ -5,7 +5,7 @@
*
* Provide GetText like language translation functions to ProcessWire
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
*/
@@ -13,6 +13,12 @@
/**
* Perform a language translation
*
* ~~~~~~
* echo __('This is translatable text');
* echo __('Translatable with current file as textdomain', __FILE__);
* echo __('Translatable with other file as textdomain', '/site/templates/_init.php');
* ~~~~~~
*
* @param string $text Text for translation.
* @param string $textdomain Textdomain for the text, may be class name, filename, or something made up by you. If omitted, a debug backtrace will attempt to determine it automatically.
* @param string $context Name of context - DO NOT USE with this function for translation as it won't be parsed for translation. Use only with the _x() function, which will be parsed.
@@ -20,14 +26,23 @@
*
*/
function __($text, $textdomain = null, $context = '') {
static $useLimit = null;
if(!wire('languages')) return $text;
if(!$language = wire('user')->language) return $text;
/** @var Language $language */
if(!$language->id) return $text;
if($useLimit === null) {
$useLimit = version_compare(PHP_VERSION, '5.4.0') >= 0;
}
if(is_null($textdomain)) {
if(defined('DEBUG_BACKTRACE_IGNORE_ARGS')) {
if($useLimit) {
// PHP 5.4.0 or newer
$traces = @debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
} else if(defined('DEBUG_BACKTRACE_IGNORE_ARGS')) {
// PHP 5.3.6 or newer
$traces = @debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
} else {
// older PHP (deprecated)
$traces = @debug_backtrace();
}
if(isset($traces[0]) && $traces[0]['file'] != __FILE__) {
@@ -57,6 +72,11 @@ function __($text, $textdomain = null, $context = '') {
*
* Used when to text strings might be the same in English, but different in other languages.
*
* ~~~~~
* echo _x('Click for more', 'button');
* echo _x('Click for more', 'text-link');
* ~~~~~
*
* @param string $text Text for translation.
* @param string $context Name of context
* @param string $textdomain Textdomain for the text, may be class name, filename, or something made up by you. If omitted, a debug backtrace will attempt to determine automatically.
@@ -70,6 +90,13 @@ function _x($text, $context, $textdomain = null) {
/**
* Perform a language translation with singular and plural versions
*
* ~~~~~
* $items = array(...);
* $qty = count($items);
* echo _n('Found one item', 'Found multiple items', $qty);
* echo sprintf(_n('Found one item', 'Found %d items', $qty), $qty);
* ~~~~~
*
* @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 of items, should be 0 or more.

View File

@@ -102,7 +102,17 @@ class MarkupFieldtype extends WireData implements Module {
$valid = false;
if($value instanceof PageArray) {
// PageArray object: get array of property value from each item
$value = $value->explode($property, array('getMethod' => 'getFormatted'));
$field = $this->wire('fields')->get($property);
if(is_object($field) && $field->type) {
$a = array();
foreach($value as $page) {
$v = $page->getFormatted($property);
$a[] = $field->type->markupValue($page, $field, $v);
}
return $this->valueToString($a);
} else {
$value = $value->explode($property, array('getMethod' => 'getFormatted'));
}
$valid = true;
} else if($value instanceof WireArray) {
@@ -115,10 +125,10 @@ 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 LanguagesValueInterface $value */
/** @var Languages $languages */
$languages = $this->wire('languages');
if($property) {
@@ -126,8 +136,10 @@ class MarkupFieldtype extends WireData implements Module {
$languageID = $languages->getDefault()->id;
} else if(is_string($property) && preg_match('/^data(\d+)$/', $property, $matches)) {
$languageID = (int) $matches[1];
} else {
$languageID = 0;
}
$value = $value->getLanguageValue($languageID);
$value = $languageID ? $value->getLanguageValue($languageID) : (string) $value;
} else {
$value = (string) $value;
}

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();
}
}
/**
@@ -411,7 +414,8 @@ class MarkupQA extends Wire {
$pageID = $pwid;
$languageID = 0;
}
$pageID = (int) $pageID;
$full = $matches[0][$key];
$start = $matches[1][$key];
$href = $matches[3][$key];
@@ -424,7 +428,7 @@ class MarkupQA extends Wire {
$language = null;
}
$livePath = $this->wire('pages')->getPath((int) $pageID, array(
$livePath = $this->wire('pages')->getPath($pageID, array(
'language' => $language
));
@@ -436,6 +440,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') . ')');
@@ -462,6 +470,108 @@ class MarkupQA extends Wire {
}
}
/**
* Find pages linking to another
*
* @param Page $page Page to find links to, or omit to use page specified in constructor
* @param array $fieldNames Field names to look in or omit to use field specified in constructor
* @param string $selector Optional selector to use as a filter
* @param array $options Additional options
* - `getIDs` (bool): Return array of page IDs rather than Page instances. (default=false)
* - `getCount` (bool): Return a total count (int) of found pages rather than Page instances. (default=false)
* - `confirm` (bool): Confirm that the links are present by looking at the actual page field data. (default=true)
* You can specify false for this option to make it perform faster, but with a potentially less accurate result.
* @return PageArray|array|int
*
*/
public function findLinks(Page $page = null, $fieldNames = array(), $selector = '', array $options = array()) {
$defaults = array(
'getIDs' => false,
'getCount' => false,
'confirm' => true
);
$options = array_merge($defaults, $options);
if($options['getIDs']) {
$result = array();
} else if($options['getCount']) {
$result = 0;
} else {
$result = $this->wire('pages')->newPageArray();
}
if(!$page) $page = $this->page;
if(!$page) return $result;
if(empty($fieldNames)) {
if($this->field) $fieldNames[] = $this->field->name;
if(empty($fieldNames)) return $result;
}
if($selector === true) $selector = "include=all";
$op = strlen("$page->id") > 3 ? "~=" : "%=";
$selector = implode('|', $fieldNames) . "$op'$page->id', id!=$page->id, $selector";
$selector = trim($selector, ', ');
// find pages
if($options['getCount'] && !$options['confirm']) {
// just return a count
return $this->wire('pages')->count($selector);
} else {
// find the IDs
$checkIDs = array();
$foundIDs = $this->wire('pages')->findIDs($selector);
if(!count($foundIDs)) return $result;
if($options['confirm']) {
$checkIDs = array_flip($foundIDs);
$foundIDs = array();
}
}
// confirm results
foreach($fieldNames as $fieldName) {
if(!count($checkIDs)) break;
$field = $this->wire('fields')->get($fieldName);
if(!$field) continue;
$table = $field->getTable();
$ids = implode(',', array_keys($checkIDs));
$sql = "SELECT * FROM `$table` WHERE `pages_id` IN($ids)";
$query = $this->wire('database')->prepare($sql);
$query->execute();
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
$pageID = (int) $row['pages_id'];
if(isset($foundIDs[$pageID])) continue;
$row = implode(' ', $row);
$find = "data-pwid=$page->id";
// first check if it might be there
if(!strpos($row, $find)) continue;
// then confirm with a more accurate check
if(!strpos($row, "$find ") && !strpos($row, "$find\t") && !strpos($row, "$find-")) continue;
// at this point we have confirmed that this item links to $page
unset($checkIDs[$pageID]);
$foundIDs[$pageID] = $pageID;
}
$query->closeCursor();
}
if(count($foundIDs)) {
if($options['getIDs']) {
$result = $foundIDs;
} else if($options['getCount']) {
$result = count($foundIDs);
} else {
$result = $this->wire('pages')->getById($foundIDs);
}
}
return $result;
}
/**
* Display and log a warning about a path that didn't resolve
*
@@ -547,7 +657,7 @@ class MarkupQA extends Wire {
list($name, $val) = explode('=', $attr);
$name = strtolower($name);
$val = trim($val, "\"' ");
$val = trim($val, "\"'> ");
if($name == 'alt' && !strlen($val)) {
$replaceAlt = $attr;

View File

@@ -5,7 +5,7 @@
*
* Provides the base interfaces required by modules.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
* #pw-summary Module is the primary PHP interface for module types in ProcessWire.
@@ -199,7 +199,7 @@
* something like an Inputfield module would not be singular. When not specified, modules that extend an
* existing base type typically inherit the singular setting from the module they extend.
*
* - `autoload` (boolean|string|callable): Should this module load automatically at boot? (default=false).
* - `autoload` (boolean|string|callable|int): Should this module load automatically at boot? (default=false).
* This is good for modules that attach hooks or that need to otherwise load on every single
* request. Autoload is typically specified as a boolean true or false. Below are the different ways
* autoload can be specified:
@@ -214,6 +214,14 @@
* - **Callable function:** The module will automatically load only if the given callable function
* returns true.
*
* - **Integer:** If given integer 2 or higher, it will autoload the module before other autoload
* modules (in /site/modules/). Higher numbers autoload before lower numbers.
*
* - `searchable` (string): When present, indicates that module implements a search() method
* consistent with the SearchableModule interface. The value of the 'searchable' property should
* be the name that the search results are referred to, using ascii characters of a-z, 0-9, and
* underscore. See the SearchableModule interface in this file for more details.
*
* -----------------------------------------------------------------------------------------------
*
* ## Module Methods
@@ -488,3 +496,95 @@ interface _Module {
public function getModuleConfigArray();
}
/**
* Interface SearchableModule
*
* Interface for modules that implement a method and expected array return value
* for completing basic text searches (primarily for admin search engine).
*
* It is optional to add this interface to "implements" section of the module class definition.
* However, you must specify a "searchable: name" property in your getModuleInfo() method in
* order for ProcessWire to recognize the module is searchable. See below for more info:
*
* ~~~~~~
* public static function getModuleInfo() {
* return array(
* 'searchable' => 'name',
*
* // You'll need the above 'searchable' property returned by your getModuleInfo().
* // The value of 'name' should be the name by which search results should be referred to
* // if the user wants to limit the search to this module. For instance, if your module
* // was called “ProcessWidgets”, youd probably choose the name “widgets” for this.
* // If the module represents an API variable, the name should be the same as the API variable.
* // ...
* );
* }
* ~~~~~
*
*/
interface SearchableModule {
/**
* Search for items containing $text and return an array representation of them
*
* You may implement also implement this method as hookable, i.e. ___search(), but not that youll
* want to skip the "implements SearchableModule" in your class definition.
*
* Must return PHP array in the format below. For each item in the 'items' array, Only the 'title'
* and 'url' properties are required for each item (the rest are optional).
*
* $result = array(
* 'title' => 'Title of these items',
* 'total' => 999, // total number of items found, or omit if pagination not supported or active
* 'url' => '', // optional URL to view all items, or omit for a PW-generated one
* 'properties' => array(), // optional list of supported search properties, only looked for if $options['info'] === true;
* 'items' => array(
* [0] => array(
* 'id' => 123, // Unique ID of item (optional)
* 'name' => 'Name of item', // (optional)
* 'title' => 'Title of item', // (*required)
* 'subtitle' => 'Secondary/subtitle of item', // (optional)
* 'summary' => 'Summary or description of item', // (optional)
* 'url' => 'URL to view or edit the item', // (*required)
* 'icon' => 'Optional icon name to represent the item, i.e. "gear" or "fa-gear"', // (optional)
* 'group' => 'Optionally group with other items having this group name, overrides $result[title]', // (optional)
* 'status' => int, // refers to Page status, omit if not a Page item (optional)
* 'modified' => int, // modified date of item as unix timestamp (optional)
* [1] => array(
* ...
* ),
* ),
* );
*
* PLEASE NOTE:
* When ProcessWire calls this method, if the module is not already loaded (autoload),
* it instantiates the module but DOES NOT call the init() or ready() methods. Thats because the
* search method is generally self contained. If you need either of those methods to be called,
* and your module is not autoload, you should call the method(s) from your search() method.
*
* About the optional “properties” index:
* If ProcessWire calls your search() method with $options['info'] == true; then it is likely wanting to see
* what properties are available for search. For instance, properties for a Module search might be:
* [ 'name', 'title', 'summary' ]. Implementation of the properties index is optional, and for PWs informational
* purposes only.
*
* @param string $text Text to search for
* @param array $options Options array provided to search() calls:
* - `edit` (bool): True if any 'url' returned should be to edit rather than view items, where access allows. (default=true)
* - `multilang` (bool): If true, search all languages rather than just current (default=true).
* - `start` (int): Start index (0-based), if pagination active (default=0).
* - `limit` (int): Limit to this many items, or 0 for no limit. (default=0).
* - `type` (string): If search should only be of a specific type, i.e. "pages", "modules", etc. then it is
* specified here. This corresponds with the getModuleInfo()['searchable'] name or item 'group' property.
* Note that ProcessWire wont call your search() method if the type cannot match this search.
* - `operator` (string): Selector operator type requested, if more than one is supported (default is %=).
* - `property` (string): If search should limit to a particular property/field, it is named here.
* - `verbose` (bool): True if output can optionally be more verbose, false if not. (default=false)
* - `debug` (bool): True if DEBUG option was specified in query. (default=false)
* - `help` (bool): True if we are just querying for help/info and are not using the search results. (default=false)
* @return array
*
*/
public function search($text, array $options = array());
}

View File

@@ -250,6 +250,22 @@ class Modules extends WireArray {
*/
protected $coreModulesDir = '';
/**
* Array of moduleName => order to indicate autoload order when necessary
*
* @var array
*
*/
protected $autoloadOrders = array();
/**
* Are we currently refreshing?
*
* @var bool
*
*/
protected $refreshing = false;
/**
* Properties that only appear in 'verbose' moduleInfo
*
@@ -264,6 +280,7 @@ class Modules extends WireArray {
'core',
'versionStr',
'permissions',
'searchable',
'page',
);
@@ -567,14 +584,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;
@@ -601,6 +623,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;
}
@@ -742,10 +765,9 @@ 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() {
$this->autoloadOrders = array();
$database = $this->wire('database');
// we use SELECT * so that this select won't be broken by future DB schema additions
// Currently: id, class, flags, data, with created added at sysupdate 7
@@ -760,7 +782,8 @@ class Modules extends WireArray {
$class = $row['class'];
$this->moduleIDs[$class] = $moduleID;
$this->moduleFlags[$moduleID] = $flags;
$loadSettings = ($flags & self::flagsAutoload) || ($flags & self::flagsDuplicate) || ($class == 'SystemUpdater');
$autoload = $flags & self::flagsAutoload;
$loadSettings = $autoload || ($flags & self::flagsDuplicate) || ($class == 'SystemUpdater');
if($loadSettings) {
// preload config data for autoload modules since we'll need it again very soon
@@ -778,6 +801,14 @@ class Modules extends WireArray {
$this->createdDates[$moduleID] = $row['created'];
}
if($autoload && !empty($this->moduleInfoCache[$moduleID]['autoload'])) {
$autoload = $this->moduleInfoCache[$moduleID]['autoload'];
if(is_int($autoload) && $autoload > 1) {
// autoload specifies an order > 1, indicating it should load before others
$this->autoloadOrders[$class] = $autoload;
}
}
unset($row['data']); // info we don't want stored in modulesTableCache
$this->modulesTableCache[$class] = $row;
}
@@ -793,12 +824,13 @@ class Modules extends WireArray {
*/
protected function load($path) {
$config = $this->wire('config');
$debugKey = $this->debug ? $this->debugTimerStart("load($path)") : null;
$installed =& $this->modulesTableCache;
$modulesLoaded = array();
$modulesDelayed = array();
$modulesRequired = array();
$rootPath = $this->wire('config')->paths->root;
$rootPath = $config->paths->root;
$basePath = substr($path, strlen($rootPath));
foreach($this->findModuleFiles($path, true) as $pathname) {
@@ -814,7 +846,7 @@ class Modules extends WireArray {
$requires = array();
$name = $moduleName;
$moduleName = $this->loadModule($path, $pathname, $requires, $installed);
if(!$this->wire('config')->paths->get($name)) $this->setConfigPaths($name, dirname($basePath . $pathname));
if(!$config->paths->get($name)) $this->setConfigPaths($name, dirname($basePath . $pathname));
if(!$moduleName) continue;
if(count($requires)) {
@@ -993,7 +1025,8 @@ class Modules extends WireArray {
} else if($autoload) {
$this->includeModuleFile($pathname, $basename);
if(!($info['flags'] & self::flagsDisabled)) {
$module = $this->newModule($basename);
$module = $this->refreshing ? parent::get($basename) : null;
if(!$module) $module = $this->newModule($basename);
}
}
}
@@ -1025,6 +1058,7 @@ class Modules extends WireArray {
static $startPath;
static $callNum = 0;
static $prependFiles = array();
$callNum++;
$config = $this->wire('config');
@@ -1048,6 +1082,11 @@ class Modules extends WireArray {
}
$files = array();
$autoloadOrders = null;
if(count($this->autoloadOrders) && $path !== $config->paths->modules) {
$autoloadOrders = &$this->autoloadOrders;
}
try {
$dir = new \DirectoryIterator($path);
@@ -1065,7 +1104,6 @@ class Modules extends WireArray {
if(DIRECTORY_SEPARATOR != '/') {
$pathname = str_replace(DIRECTORY_SEPARATOR, '/', $pathname);
$filename = str_replace(DIRECTORY_SEPARATOR, '/', $filename);
}
if(strpos($pathname, '/.') !== false) {
@@ -1079,16 +1117,30 @@ class Modules extends WireArray {
}
// if the filename doesn't end with .module or .module.php, then stop and move onto the next
if(!strpos($filename, '.module')) continue;
if(substr($filename, -7) !== '.module' && substr($filename, -11) !== '.module.php') {
continue;
$extension = $file->getExtension();
if($extension !== 'module' && $extension !== 'php') continue;
list($moduleName, $extension) = explode('.', $filename, 2);
if($extension !== 'module' && $extension !== 'module.php') continue;
$pathname = str_replace($startPath, '', $pathname);
if($autoloadOrders !== null && isset($autoloadOrders[$moduleName])) {
$prependFiles[$pathname] = $autoloadOrders[$moduleName];
} else {
$files[] = $pathname;
}
$files[] = str_replace($startPath, '', $pathname);
}
if($level == 0 && $dir !== null) {
if($cache && $cacheName) $cache->save($cacheName, implode("\n", $files), WireCache::expireNever);
if(!empty($prependFiles)) {
// one or more non-core modules must be loaded first in a specific order
arsort($prependFiles);
$files = array_merge(array_keys($prependFiles), $files);
$prependFiles = array();
}
if($cache && $cacheName) {
$cache->save($cacheName, implode("\n", $files), WireCache::expireNever);
}
}
return $files;
@@ -1169,11 +1221,13 @@ class Modules extends WireArray {
* This is the same as `$modules->get()` except that you can specify additional options to modify default behavior.
* 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
@@ -1181,69 +1235,110 @@ 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";
}
} catch(\Exception $e) {
if(empty($options['noThrow'])) throw $e;
return empty($options['returnError']) ? null : "Module '$module' throw Exception on init - " . $e->getMessage();
}
}
@@ -1460,7 +1555,7 @@ class Modules extends WireArray {
* #pw-internal Almost always recommend using findByPrefix() instead
*
* @param string $selector Selector string
* @return Modules WireArray of found modules, instantiated and ready-to-use
* @return WireArray of found modules, instantiated and ready-to-use
*
*/
public function find($selector) {
@@ -1490,17 +1585,25 @@ class Modules extends WireArray {
* ~~~~~
*
* @param string $prefix Specify prefix, i.e. "Process", "Fieldtype", "Inputfield", etc.
* @param bool $instantiate Specify true to return Module instances, or false to return class names (default=false)
* @param bool|int $load Specify one of the following:
* - Boolean true to return array of instantiated modules.
* - Boolean false to return array of module names (default).
* - Integer 1 to return array of module info for each matching module.
* - Integer 2 to return array of verbose module info for each matching module.
* @return array Returns array of module class names or Module objects. In either case, array indexes are class names.
*
*/
public function findByPrefix($prefix, $instantiate = false) {
public function findByPrefix($prefix, $load = false) {
$results = array();
foreach($this as $key => $value) {
$className = wireClassName($value->className(), false);
if(strpos($className, $prefix) !== 0) continue;
if($instantiate) {
$results[$className] = $this->get($className);
if($load === 1) {
$results[$className] = $this->getModuleInfo($className);
} else if($load === 2) {
$results[$className] = $this->getModuleInfoVerbose($className);
} else if($load === true) {
$results[$className] = $this->getModule($className);
} else {
$results[$className] = $className;
}
@@ -1508,6 +1611,110 @@ class Modules extends WireArray {
return $results;
}
/**
* Find modules by matching a property or properties in their module info
*
* @param string|array $selector Specify one of the following:
* - Selector string to match module info.
* - Array of [ 'property' => 'value' ] to match in module info (this is not a selector array).
* - Name of property that will match module if not empty in module info.
* @param bool|int $load Specify one of the following:
* - Boolean true to return array of instantiated modules.
* - Boolean false to return array of module names (default).
* - Integer 1 to return array of module info for each matching module.
* - Integer 2 to return array of verbose module info for each matching module.
* @return array Array of modules, module names or module info arrays, indexed by module name.
*
*/
public function findByInfo($selector, $load = false) {
$selectors = null;
$infos = null;
$keys = null;
$results = array();
$verbose = $load === 2;
$properties = array();
$has = '';
if(is_array($selector)) {
// find matching all values in array
$keys = $selector;
$properties = array_keys($keys);
} if(!ctype_alnum($selector) && Selectors::stringHasOperator($selector)) {
// find by selectors
$selectors = new Selectors($selector);
if(!$verbose) foreach($selectors as $s) {
$properties = array_merge($properties, $s->fields());
}
} else {
// find non-empty property
$has = $selector;
$properties[] = $has;
}
// check if any verbose properties are part of the find
if(!$verbose) foreach($properties as $property) {
if(!in_array($property, $this->moduleInfoVerboseKeys)) continue;
$verbose = true;
break;
}
$moduleInfoOptions = array(
'verbose' => $verbose,
'minify' => false
);
foreach($this->getModuleInfo('*', $moduleInfoOptions) as $info) {
$isMatch = false;
if($has) {
// simply check if property is non-empty
if(!empty($info[$has])) $isMatch = true;
} else if($selectors) {
// match selector
$total = 0;
$n = 0;
foreach($selectors as $selector) {
$total++;
$values = array();
foreach($selector->fields() as $property) {
if(isset($info[$property])) $values[] = $info[$property];
}
if($selector->matches($values)) $n++;
}
if($n && $n === $total) $isMatch = true;
} else if($keys) {
// match all values in $keys array
$n = 0;
foreach($keys as $key => $value) {
if($value === '*') {
// match any non-empty value
if(!empty($info[$key])) $n++;
} else {
// match exact value
if(isset($info[$key]) && $value == $info[$key]) $n++;
}
}
if($n && $n === count($keys)) $isMatch = true;
}
if($isMatch) {
$moduleName = $info['name'];
if(is_int($load)) {
$results[$moduleName] = $info;
} else if($load === true) {
$results[$moduleName] = $this->getModule($moduleName);
} else {
$results[$moduleName] = $moduleName;
}
}
}
return $results;
}
/**
* Get an associative array [name => path] for all modules that arent currently installed.
*
@@ -1861,6 +2068,7 @@ class Modules extends WireArray {
$success = false;
$reason = $this->isDeleteable($class, true);
if($reason !== true) throw new WireException($reason);
$siteModulesPath = $this->wire('config')->paths->siteModules;
$filename = $this->installable[$class];
$basename = basename($filename);
@@ -1954,7 +2162,7 @@ class Modules extends WireArray {
foreach($files as $file) {
$file = "$path/$file";
if(!file_exists($file)) continue;
if(unlink($file)) {
if($this->wire('files')->unlink($file, $siteModulesPath)) {
$this->message("Removed file: $file", Notice::debug);
} else {
$this->error("Unable to remove file: $file", Notice::debug);
@@ -2372,13 +2580,14 @@ class Modules extends WireArray {
* @param $className
* @return array Only includes module info specified in the module file itself.
*
*/
protected function getModuleInfoInternalSafe($className) {
// future addition
// load file, preg_split by /^\s*(public|private|protected)[^;{]+function\s*([^)]*)[^{]*{/
// isolate the one that starts has getModuleInfo in matches[1]
// parse data from matches[2]
return array();
}
*/
/**
* Retrieve module info for system properties: PHP or ProcessWire
@@ -2429,19 +2638,20 @@ class Modules extends WireArray {
* - `name` (string): module class name.
* - `title` (string): module title.
* - `version` (int): module version.
* - `icon` (string): Optional icon name (excluding the "fa - ") part.
* - `icon` (string): Optional icon name (excluding the "fa-") part.
* - `requires` (array): module names required by this module.
* - `requiresVersions` (array): required module versionsmodule name is key, value is array($operator, $version).
* - `installs` (array): module names that this module installs.
* - `permission` (string): permission name required to execute this module.
* - `autoload` (bool): true if module is autoload, false if not.
* - `singular` (bool): true if module is singular, false if not.
* - `created` (int): unix - timestamp of date/time module added to system (for uninstalled modules, it is the file date).
* - `created` (int): unix-timestamp of date/time module added to system (for uninstalled modules, it is the file date).
* - `installed` (bool): is the module currently installed? (boolean, or null when not determined)
* - `configurable` (bool|int): true or positive number when the module is configurable.
* - `namespace` (string): PHP namespace that module lives in.
*
* The following properties are also included when "verbose" mode is requested. When not in verbose mode, these properties are present but blank:
* The following properties are also included when "verbose" mode is requested. When not in verbose mode, these
* properties are present but blank:
*
* - `versionStr` (string): formatted module version string.
* - `file` (string): module filename from PW installation root, or false when it can't be found.
@@ -2449,10 +2659,11 @@ class Modules extends WireArray {
* - `author` (string): module author, when specified.
* - `summary` (string): summary of what this module does.
* - `href` (string): URL to module details (when specified).
* - `permissions` (array): permissions installed by this module, associative array ('permission - name' => 'Description').
* - `permissions` (array): permissions installed by this module, associative array ('permission-name' => 'Description').
* - `page` (array): definition of page to create for Process module (see Process class)
*
* The following properties appear only for "Process" modules. See the Process class for more details:
* The following properties appear only for "Process" modules, and only if specified by module.
* See the Process class for more details:
*
* - `nav` (array): navigation definition
* - `useNavJSON` (bool): whether the Process module provides JSON navigation
@@ -2467,12 +2678,17 @@ class Modules extends WireArray {
* $moduleInfo = $modules->getModuleInfoVerbose('MarkupAdminDataTable');
* ~~~~~
*
* @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 string|Module|int $class Specify one of the following:
* - Module object instance
* - Module class name (string)
* - Module ID (int)
* - To get info for ALL modules, specify `*` or `all`.
* - To get system information, specify `ProcessWire` or `PHP`.
* - To get a blank module info template, specify `info`.
* @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.
* - `noInclude` (bool): prevents include() of the module file, applicable only if it hasn't already been included.
* - `verbose` (bool): Makes the info also include verbose properties, which are otherwise blank. (default=false)
* - `minify` (bool): Remove non-applicable and properties that match defaults? (default=false, or true when getting `all`)
* - `noCache` (bool): prevents use of cache to retrieve the module info. (default=false)
* @return array Associative array of module information
* @throws WireException when a module exists but has no means of returning module info
* @see Modules::getModuleInfoVerbose()
@@ -2480,15 +2696,28 @@ class Modules extends WireArray {
*
*/
public function getModuleInfo($class, array $options = array()) {
if(!isset($options['verbose'])) $options['verbose'] = false;
if(!isset($options['noCache'])) $options['noCache'] = false;
$getAll = $class === '*' || $class === 'all';
$getSystem = $class === 'ProcessWire' || $class === 'PHP' || $class === 'info';
$defaults = array(
'verbose' => false,
'minify' => $getAll,
'noCache' => false,
'noInclude' => false,
);
$options = array_merge($defaults, $options);
$info = array();
$module = $class;
$moduleName = $this->getModuleClass($module);
$moduleID = (string) $this->getModuleID($module); // typecast to string for cache
$moduleName = '';
$moduleID = 0;
$fromCache = false; // was the data loaded from cache?
if(!$getAll && !$getSystem) {
$moduleName = $this->getModuleClass($module);
$moduleID = (string) $this->getModuleID($module); // typecast to string for cache
}
static $infoTemplate = array(
// module database ID
@@ -2528,7 +2757,9 @@ class Modules extends WireArray {
// is the module currently installed? (boolean, or null when not determined)
'installed' => null,
// this is set to true when the module is configurable, false when it's not, and null when it's not determined
'configurable' => null,
'configurable' => null,
// verbose mode only: true when module implements SearchableModule interface, or null|false when not
'searchable' => null,
// namespace that module lives in (string)
'namespace' => null,
// verbose mode only: this is set to the module filename (from PW installation root), false when it can't be found, null when it hasn't been determined
@@ -2542,8 +2773,30 @@ class Modules extends WireArray {
// 'page' => array(), // page to create for Process module: see Process.php
// 'permissionMethod' => string or callable // method to call to determine permission: see Process.php
);
if($module instanceof Module) {
if($getAll) {
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);
}
}
if(!$options['minify']) {
foreach($modulesInfo as $moduleID => $info) {
$modulesInfo[$moduleID] = array_merge($infoTemplate, $info);
}
}
return $modulesInfo;
} else if($getSystem) {
// module is a system
if($class === 'info') return $infoTemplate;
$info = $this->getModuleInfoSystem($module);
return $options['minify'] ? $info : array_merge($infoTemplate, $info);
} else if($module instanceof Module) {
// module is an instance
// $moduleName = method_exists($module, 'className') ? $module->className() : get_class($module);
// return from cache if available
@@ -2556,28 +2809,12 @@ class Modules extends WireArray {
if(!count($info)) $info = $this->getModuleInfoInternal($module);
}
} else if($module == 'PHP' || $module == 'ProcessWire') {
// module is a system
$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
if(ctype_digit("$module")) $module = $moduleName;
// return from cache if available
// return from cache if available (as it almost always should be)
if(empty($options['noCache']) && !empty($this->moduleInfoCache[$moduleID])) {
$info = $this->moduleInfoCache[$moduleID];
$fromCache = true;
@@ -2618,10 +2855,11 @@ class Modules extends WireArray {
return $info;
}
$info = array_merge($infoTemplate, $info);
if(!$options['minify']) $info = array_merge($infoTemplate, $info);
$info['id'] = (int) $moduleID;
if($fromCache) {
// since cache is loaded at init(), this is the most common scenario
if($options['verbose']) {
if(empty($this->moduleInfoCacheVerbose)) $this->loadModuleInfoCacheVerbose();
@@ -2635,7 +2873,7 @@ class Modules extends WireArray {
if(is_null($info['singular'])) $info['singular'] = false;
if(is_null($info['configurable'])) $info['configurable'] = false;
if(is_null($info['core'])) $info['core'] = false;
if(is_null($info['installed'])) $info['installed'] = true;
if(is_null($info['installed'])) $info['installed'] = true;
if(is_null($info['namespace'])) $info['namespace'] = strlen(__NAMESPACE__) ? "\\" . __NAMESPACE__ . "\\" : "";
if(!empty($info['requiresVersions'])) $info['requires'] = array_keys($info['requiresVersions']);
if($moduleName == 'SystemUpdater') $info['configurable'] = 1; // fallback, just in case
@@ -2644,12 +2882,16 @@ class Modules extends WireArray {
// are already accounted for in the cached module info
} else {
// if $info[requires] or $info[installs] isn't already an array, make it one
// not from cache, only likely to occur when refreshing modules info caches
// if $info[requires] 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
@@ -2667,14 +2909,18 @@ class Modules extends WireArray {
}
// what does it install?
// if $info[installs] isn't already an array, make it one
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
$info['versionStr'] = $this->formatVersion($info['version']); // versionStr
if($options['verbose']) $info['versionStr'] = $this->formatVersion($info['version']); // versionStr
$info['name'] = $moduleName; // module name
// module configurable?
@@ -2730,6 +2976,18 @@ class Modules extends WireArray {
// the file property is not stored in the verbose cache, but provided as a verbose key
$info['file'] = $this->getModuleFile($moduleName);
if($info['file']) $info['core'] = strpos($info['file'], $this->coreModulesDir) !== false; // is it core?
} else {
// module info may still contain verbose keys with undefined values
}
if($options['minify']) {
// when minify, any values that match defaults from infoTemplate are removed
if(!$options['verbose']) foreach($this->moduleInfoVerboseKeys as $key) unset($info[$key]);
foreach($info as $key => $value) {
if(!array_key_exists($key, $infoTemplate)) continue;
if($value !== $infoTemplate[$key]) continue;
unset($info[$key]);
}
}
// if($this->debug) $this->message("getModuleInfo($moduleName) " . ($fromCache ? "CACHE" : "NO-CACHE"));
@@ -2740,7 +2998,7 @@ class Modules extends WireArray {
/**
* Returns a verbose array of information for a Module
*
* This is the same as what's returned by `Modules::getModuleInfo()` except that it has the following additional properties:
* This is the same as whats returned by `Modules::getModuleInfo()` except that it has the following additional properties:
*
* - `versionStr` (string): formatted module version string.
* - `file` (string): module filename from PW installation root, or false when it can't be found.
@@ -2766,6 +3024,29 @@ class Modules extends WireArray {
return $info;
}
/**
* Get just a single property of module info
*
* @param Module|string $class Module instance or module name
* @param string $property Name of property to get
* @param array $options Additional options (see getModuleInfo method for options)
* @return mixed|null Returns value of property or null if not found
*
*/
public function getModuleInfoProperty($class, $property, array $options = array()) {
if(isset($this->moduleInfoVerboseKeys[$property])) {
$info = $this->getModuleInfoVerbose($class, $options);
$info['verbose'] = true;
} else {
$info = $this->getModuleInfo($class, $options);
}
if(!isset($info[$property]) && empty($info['verbose'])) {
// try again, just in case we can find it in verbose data
$info = $this->getModuleInfoVerbose($class, $options);
}
return isset($info[$property]) ? $info[$property] : null;
}
/**
* Get an array of all unique, non-default, non-root module namespaces mapped to directory names
*
@@ -2937,6 +3218,27 @@ class Modules extends WireArray {
public function getModuleConfigData($className) {
return $this->getConfig($className);
}
/**
* Return the URL where the module can be edited, configured or uninstalled
*
* If module is not installed, it just returns the URL to ProcessModule.
*
* #pw-group-configuration
*
* @param string|Module $className
* @param bool $collapseInfo
* @return string
*
*/
public function getModuleEditUrl($className, $collapseInfo = true) {
if(!is_string($className)) $className = $this->getModuleClass($className);
$url = $this->wire('config')->urls->admin . 'module/';
if(empty($className) || !$this->isInstalled($className)) return $url;
$url .= "edit/?name=$className";
if($collapseInfo) $url .= "&collapse_info=1";
return $url;
}
/**
* Given a module name, return an associative array of configuration data for it
@@ -3561,6 +3863,7 @@ class Modules extends WireArray {
// we allow for option of no return statement in the method
$module = $this->getModule($moduleName);
$fields = $this->wire(new InputfieldWrapper());
$fields->setParent($form);
$_fields = $module->getModuleConfigInputfields($fields);
if($_fields instanceof InputfieldWrapper) $fields = $_fields;
unset($_fields);
@@ -3850,10 +4153,13 @@ class Modules extends WireArray {
if($this->wire('config')->systemVersion < 6) {
return;
}
$this->refreshing = true;
$this->clearModuleInfoCache();
$this->loadModulesTable();
foreach($this->paths as $path) $this->findModuleFiles($path, false);
foreach($this->paths as $path) $this->load($path);
if($this->duplicates()->numNewDuplicates() > 0) $this->duplicates()->updateDuplicates(); // PR#1020
$this->refreshing = false;
}
/**
@@ -4639,7 +4945,7 @@ class Modules extends WireArray {
* #pw-internal
*
* @param Module|int|string $module Module object or class name
* @return array Returns number of files that were added
* @return int Returns number of files that were added
*
*/
public function loadModuleFileAssets($module) {
@@ -4717,7 +5023,7 @@ class Modules extends WireArray {
*
* @param array|Wire|string $text
* @param int $flags
* @return $this
* @return Modules|WireArray
*
*/
public function error($text, $flags = 0) {

View File

@@ -307,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

@@ -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 2018 by Ryan Cramer
* https://processwire.com
*
* #pw-summary Class used by all Page objects in ProcessWire.
@@ -29,9 +29,10 @@
* @property int $id The numbered ID of the current page #pw-group-system
* @property string $name The name assigned to the page, as it appears in the URL #pw-group-system #pw-group-common
* @property string $namePrevious Previous name, if changed. Blank if not. #pw-advanced
* @property string $title The page's title (headline) text
* @property string $path The page's URL path from the homepage (i.e. /about/staff/ryan/)
* @property string $url The page's URL path from the server's document root
* @property string $title The pages title (headline) text
* @property string $path The pages URL path from the homepage (i.e. /about/staff/ryan/)
* @property string $url The pages URL path from the server's document root
* @property array $urls All URLs the page is accessible from, whether current, former and multi-language. #pw-group-urls
* @property string $httpUrl Same as $page->url, except includes scheme (http or https) and hostname.
* @property Page|string|int $parent The parent Page object or a NullPage if there is no parent. For assignment, you may also use the parent path (string) or id (integer). #pw-group-traversal
* @property Page|null $parentPrevious Previous parent, if parent was changed. #pw-group-traversal
@@ -45,6 +46,8 @@
* @property int $numChildren The number of children (subpages) this page has, with no exclusions (fast). #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 int $numDescendants Number of descendants (quantity of children, and their children, and so on). @since 3.0.116 #pw-group-traversal
* @property int $numParents Number of parent pages (i.e. depth) @since 3.0.117 #pw-group-traversal
* @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
@@ -70,11 +73,17 @@
* @property int|null $statusPrevious Previous status, if status was changed. #pw-group-status
* @property string statusStr Returns space-separated string of status names active on this page. #pw-group-status
* @property Fieldgroup $fieldgroup Fieldgroup used by page template. Shorter alias for $page->template->fieldgroup (same as $page->fields) #pw-advanced
* @property string $editUrl URL that this page can be edited at. #pw-group-advanced
* @property string $editUrl URL that this page can be edited at. #pw-group-urls
* @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 int $numReferences Total number of pages referencing this page with Page reference fields. #pw-group-traversal
* @property int $hasReferences Number of visible pages (to current user) referencing this page with page reference fields. #pw-group-traversal
* @property int $numReferencing Total number of other pages this page is pointing to (referencing) with Page fields. #pw-group-traversal
* @property int $numLinks Total number of pages manually linking to this page in Textarea/HTML fields. #pw-group-traversal
* @property int $hasLinks Number of visible pages (to current user) linking to this page in Textarea/HTML fields. #pw-group-traversal
*
* @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
*
@@ -91,6 +100,7 @@
* @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($orDeleteable = false) Returns true if the page is trashable by the current user, false if not. #pw-group-access
* @method bool restorable() Returns true if page is in the trash and is capable of being restored to its original location. @since 3.0.107 #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
@@ -104,7 +114,13 @@
* @property bool $moveable #pw-group-access
* @property bool $sortable #pw-group-access
* @property bool $listable #pw-group-access
*
*
* Methods added by PagePathHistory.module (installed by default)
* --------------------------------------------------------------
* @method bool addUrl($url, $language = null) Add a new URL that redirects to this page and save immediately (returns false if already taken). #pw-group-urls #pw-group-manipulation
* @method bool removeUrl($url) Remove a URL that redirects to this page and save immediately. #pw-group-urls #pw-group-manipulation
* Note: you can use the $page->urls() method to get URLs added by PagePathHistory.
*
* Methods added by LanguageSupport.module (not installed by default)
* -----------------------------------------------------------------
* @method Page setLanguageValue($language, $field, $value) Set value for field in language (requires LanguageSupport module). $language may be ID, language name or Language object. Field should be field name (string). #pw-group-languages
@@ -112,10 +128,10 @@
*
* 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 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
* @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 #pw-group-urls
* @method string localUrl($language = null) Return the page URL in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages #pw-group-urls
* @method string localHttpUrl($language = null) Return the page URL (including scheme and hostname) in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages #pw-group-urls
*
* Methods added by ProDrafts.module (if installed)
* ------------------------------------------------
@@ -131,6 +147,13 @@
* @method string getMarkup($key) Return the markup value for a given field name or {tag} string. #pw-internal
* @method string|mixed renderField($fieldName, $file = '') Returns rendered field markup, optionally with file relative to templates/fields/. #pw-internal
* @method string|mixed renderValue($value, $file) Returns rendered markup for $value using $file relative to templates/fields/. #pw-internal
* @method PageArray references($selector = '', $field = '') Return pages that are pointing to this one by way of Page reference fields. #pw-group-traversal
* @method PageArray links($selector = '', $field = '') Return pages that link to this one contextually in Textarea/HTML fields. #pw-group-traversal
*
* Alias/alternate methods
* -----------------------
* @method PageArray descendants($selector = '', array $options = array()) Find descendant pages, alias of `Page::find()`, see that method for details. @since 3.0.116 #pw-group-traversal
* @method Page|NullPage descendant($selector = '', array $options = array()) Find one descendant page, alias of `Page::findOne()`, see that method for details. @since 3.0.116 #pw-group-traversal
*
*/
@@ -177,6 +200,12 @@ class Page extends WireData implements \Countable, WireMatchable {
*/
const statusSystem = 16;
/**
* Page has a globally unique name and no other pages may have the same name
*
*/
const statusUnique = 32;
/**
* Page has pending draft changes (name: "draft").
* #pw-internal
@@ -302,6 +331,14 @@ class Page extends WireData implements \Countable, WireMatchable {
*/
protected $_parent_id = 0;
/**
* Traversal siblings/items set by setTraversalItems() to force usage in some page traversal calls
*
* @var PageArray|null
*
*/
protected $traversalPages = null;
/**
* The previous parent used by the page, if it was changed during runtime.
*
@@ -357,6 +394,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)?
*
@@ -524,6 +571,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* - "p": Property name maps to same property name in $this
* - "m": Property name maps to same method name in $this
* - "n": Property name maps to same method name in $this, but may be overridden by custom field
* - "t": Property name maps to PageTraversal method with same name, if not overridden by custom field
* - [blank]: needs additional logic to be handled ([blank]='')
*
* @var array
@@ -544,8 +592,10 @@ class Page extends WireData implements \Countable, WireMatchable {
'editUrl' => 'm',
'fieldgroup' => '',
'filesManager' => 'm',
'hasParent' => 'parents',
'hasChildren' => 'm',
'hasLinks' => 't',
'hasParent' => 'parents',
'hasReferences' => 't',
'httpUrl' => 'm',
'id' => 's',
'index' => 'n',
@@ -557,6 +607,7 @@ class Page extends WireData implements \Countable, WireMatchable {
'isPublic' => 'm',
'isTrash' => 'm',
'isUnpublished' => 'm',
'links' => 'n',
'listable' => 'm',
'modified' => 's',
'modifiedStr' => '',
@@ -567,6 +618,10 @@ class Page extends WireData implements \Countable, WireMatchable {
'namePrevious' => 'p',
'next' => 'm',
'numChildren' => 's',
'numParents' => 'm',
'numDescendants' => 'm',
'numLinks' => 't',
'numReferences' => 't',
'output' => 'm',
'outputFormatting' => 'p',
'parent' => 'm',
@@ -578,6 +633,8 @@ class Page extends WireData implements \Countable, WireMatchable {
'publishable' => 'm',
'published' => 's',
'publishedStr' => '',
'references' => 'n',
'referencing' => 't',
'render' => '',
'rootParent' => 'm',
'siblings' => 'm',
@@ -592,6 +649,7 @@ class Page extends WireData implements \Countable, WireMatchable {
'templatePrevious' => 'p',
'trashable' => 'm',
'url' => 'm',
'urls' => 'm',
'viewable' => 'm'
);
@@ -626,6 +684,17 @@ class Page extends WireData implements \Countable, WireMatchable {
'templatesID' => 'templates_id',
);
/**
* Method alternates/aliases (alias => actual)
*
* @var array
*
*/
static $baseMethodAlternates = array(
'descendants' => 'find',
'descendant' => 'findOne',
);
/**
* Create a new page in memory.
*
@@ -874,7 +943,7 @@ class Page extends WireData implements \Countable, WireMatchable {
// if the page is not yet loaded and a '__' field was set, then we queue it so that the loaded() method can
// instantiate all those fields knowing that all parts of them are present for wakeup.
if(!$this->isLoaded && strpos($key, '__')) {
list($key, $subKey) = explode('__', $key);
list($key, $subKey) = explode('__', $key, 2);
if(!isset($this->fieldDataQueue[$key])) $this->fieldDataQueue[$key] = array();
$this->fieldDataQueue[$key][$subKey] = $value;
return $this;
@@ -898,11 +967,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);
$value = $field->type->_callHookMethod('wakeupValue', array($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);
}
@@ -911,7 +977,17 @@ 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 {
// check if the field is corrupted
@@ -988,18 +1064,21 @@ class Page extends WireData implements \Countable, WireMatchable {
if(isset(self::$basePropertiesAlternates[$key])) $key = self::$basePropertiesAlternates[$key];
if(isset(self::$baseProperties[$key])) {
$type = self::$baseProperties[$key];
if($type == 'p') {
if($type === 'p') {
// local property
return $this->$key;
} else if($type == 'm') {
} else if($type === 'm') {
// local method
return $this->{$key}();
} else if($type == 'n') {
} else if($type === 'n') {
// local method, possibly overridden by $field
if(!$this->wire('fields')->get($key)) return $this->{$key}();
} else if($type == 's') {
} else if($type === 's') {
// settings property
return $this->settings[$key];
} else if($type === 't') {
// map to method in PageTraversal, if not overridden by field
if(!$this->wire('fields')->get($key)) return $this->traversal()->{$key}($this);
} else if($type) {
// defined local method
return $this->{$type}();
@@ -1018,7 +1097,7 @@ class Page extends WireData implements \Countable, WireMatchable {
$value = $this->template ? $this->template->id : 0;
break;
case 'fieldgroup':
$value = $this->template->fieldgroup;
$value = $this->template ? $this->template->fieldgroup : null;
break;
case 'modifiedUser':
case 'createdUser':
@@ -1098,7 +1177,6 @@ class Page extends WireData implements \Countable, WireMatchable {
*
* @param string|int|Field $field
* @return Field|null
* @throws WireException if given invalid argument
* @todo determine if we can always retrieve in context regardless of output formatting.
*
*/
@@ -1130,6 +1208,7 @@ class Page extends WireData implements \Countable, WireMatchable {
public function getFields() {
if(!$this->template) return new FieldsArray();
$fields = new FieldsArray();
/** @var Fieldgroup $fieldgroup */
$fieldgroup = $this->template->fieldgroup;
foreach($fieldgroup as $field) {
if($fieldgroup->hasFieldContext($field)) {
@@ -1287,7 +1366,17 @@ class Page extends WireData implements \Countable, WireMatchable {
$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) {
@@ -1450,7 +1539,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));
@@ -1481,7 +1572,8 @@ class Page extends WireData implements \Countable, WireMatchable {
*/
public function getText($key, $oneLine = false, $entities = null) {
$value = $this->getMarkup($key);
if(!strlen($value)) return '';
$length = strlen($value);
if(!$length) return '';
$options = array(
'entities' => (is_null($entities) ? $this->outputFormatting() : (bool) $entities)
);
@@ -1490,6 +1582,8 @@ class Page extends WireData implements \Countable, WireMatchable {
} else {
$value = $this->wire('sanitizer')->markupToText($value, $options);
}
// if stripping tags from non-empty value made it empty, just indicate that it was markup and length
if(!strlen(trim($value))) $value = "markup($length)";
return $value;
}
@@ -1601,8 +1695,10 @@ class Page extends WireData implements \Countable, WireMatchable {
if(count($arguments)) {
return $this->getFieldValue($method, $arguments[0]);
} else {
return $this->get($method);
return $this->get($method);
}
} else if(isset(self::$baseMethodAlternates[$method])) {
return call_user_func_array(array($this, self::$baseMethodAlternates[$method]), $arguments);
} else {
return parent::___callUnknown($method, $arguments);
}
@@ -1865,7 +1961,7 @@ class Page extends WireData implements \Countable, WireMatchable {
}
/**
* Find pages matching given selector in the descendent hierarchy
* Find descendant pages matching given selector
*
* This is the same as `Pages::find()` except that the results are limited to descendents of this Page.
*
@@ -1892,6 +1988,36 @@ class Page extends WireData implements \Countable, WireMatchable {
}
return $this->_pages('find', $selector, $options);
}
/**
* Find one descendant page matching given selector
*
* This is the same as `Pages::findOne()` except that the match is always a descendant of page it is called on.
*
* ~~~~~
* // Find the most recently modified descendant page
* $item = $page->findOne("sort=-modified");
* ~~~~~
*
* #pw-group-common
* #pw-group-traversal
*
* @param string|array $selector Selector string or array
* @param array $options Optional options to modify default bheavior, see options for `Pages::find()`.
* @return Page|NullPage Returns Page when found, or NullPage when nothing found.
* @see Pages::findOne(), Page::child()
* @since 3.0.116
*
*/
public function findOne($selector = '', $options = array()) {
if(!$this->numChildren) return $this->wire('pages')->newNullPage();
if(is_string($selector)) {
$selector = trim("has_parent={$this->id}, $selector", ", ");
} else if(is_array($selector)) {
$selector["has_parent"] = $this->id;
}
return $this->_pages('findOne', $selector, $options);
}
/**
* Return this pages children, optionally filtered by a selector
@@ -2072,6 +2198,20 @@ class Page extends WireData implements \Countable, WireMatchable {
return $this->traversal()->parents($this, $selector);
}
/**
* Return number of parents (depth relative to homepage) that this page has, optionally filtered by a selector
*
* For example, homepage has 0 parents and root level pages have 1 parent (which is the homepage), and the
* number increases the deeper the page is in the pages structure.
*
* @param string $selector Optional selector to filter by (default='')
* @return int Number of parents
*
*/
public function numParents($selector = '') {
return $this->traversal()->numParents($this, $selector);
}
/**
* Return all parents from current page till the one matched by $selector
*
@@ -2172,6 +2312,35 @@ class Page extends WireData implements \Countable, WireMatchable {
}
return $this->traversal()->siblings($this, $selector);
}
/**
* Return number of descendants (children, grandchildren, great-grandchildren, …), optionally with conditions
*
* Use this over the `$page->numDescendants` property when you want to specify a selector or apply
* some other filter to the result (see options for `$selector` argument). If you want to include only
* visible descendants specify a selector (string or array) or boolean true for the `$selector` argument,
* if you dont need a selector.
*
* If you want to find descendant pages (rather than count), use the `Page::find()` method.
*
* ~~~~~
* // Find how many descendants were modified in the last week
* $qty = $page->numDescendants("modified>='-1 WEEK'");
* ~~~~~
*
* #pw-group-traversal
*
* @param bool|string|array $selector
* - When not specified, result includes all descendants without conditions, same as $page->numDescendants property.
* - When a string or array, a selector is assumed and quantity will be counted based on selector.
* - When boolean true, number includes only visible descendants (excludes unpublished, hidden, no-access, etc.)
* @return int Number of descendants
* @see Page::numChildren(), Page::find()
*
*/
public function numDescendants($selector = null) {
return $this->traversal()->numDescendants($this, $selector);
}
/**
* Return the next sibling page
@@ -2193,7 +2362,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* #pw-group-traversal
*
* @param string|array $selector Optional selector. When specified, will find nearest next sibling that matches.
* @param PageArray $siblings DEPRECATED: Optional siblings to use instead of the default. Avoid using this argument
* @param PageArray $siblings Optional siblings to use instead of the default. Avoid using this argument
* as it forces this method to use the older/slower functions.
* @return Page|NullPage Returns the next sibling page, or a NullPage if none found.
*
@@ -2203,6 +2372,7 @@ class Page extends WireData implements \Countable, WireMatchable {
$siblings = $selector;
$selector = '';
}
if($siblings === null && $this->traversalPages) $siblings = $this->traversalPages;
if($siblings) return $this->traversal()->nextSibling($this, $selector, $siblings);
return $this->traversal()->next($this, $selector);
}
@@ -2234,6 +2404,7 @@ class Page extends WireData implements \Countable, WireMatchable {
$getQty = $selector;
$selector = '';
}
if($siblings === null && $this->traversalPages) $siblings = $this->traversalPages;
if($getPrev) {
if($siblings) return $this->traversal()->prevAllSiblings($this, $selector, $siblings);
return $this->traversal()->prevAll($this, $selector, array('qty' => $getQty));
@@ -2249,11 +2420,12 @@ class Page extends WireData implements \Countable, WireMatchable {
*
* @param string|Page|array $selector May either be a selector or Page to stop at. Results will not include this.
* @param string|array $filter Optional selector to filter matched pages by
* @param PageArray $siblings DEPRECATED: Optional PageArray of siblings to use instead (avoid).
* @param PageArray $siblings Optional PageArray of siblings to use instead (avoid).
* @return PageArray
*
*/
public function nextUntil($selector = '', $filter = '', PageArray $siblings = null) {
if($siblings === null && $this->traversalPages) $siblings = $this->traversalPages;
if($siblings) return $this->traversal()->nextUntilSiblings($this, $selector, $filter, $siblings);
return $this->traversal()->nextUntil($this, $selector, $filter);
}
@@ -2272,7 +2444,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* #pw-group-traversal
*
* @param string|array $selector Optional selector. When specified, will find nearest previous sibling that matches.
* @param PageArray|null $siblings DEPRECATED: $siblings Optional siblings to use instead of the default.
* @param PageArray|null $siblings Optional siblings to use instead of the default.
* @return Page|NullPage Returns the previous sibling page, or a NullPage if none found.
*
*/
@@ -2281,6 +2453,7 @@ class Page extends WireData implements \Countable, WireMatchable {
$siblings = $selector;
$selector = '';
}
if($siblings === null && $this->traversalPages) $siblings = $this->traversalPages;
if($siblings) return $this->traversal()->prevSibling($this, $selector, $siblings);
return $this->traversal()->prev($this, $selector);
}
@@ -2308,14 +2481,53 @@ class Page extends WireData implements \Countable, WireMatchable {
*
* @param string|Page|array $selector May either be a selector or Page to stop at. Results will not include this.
* @param string|array $filter Optional selector to filter matched pages by
* @param PageArray|null $siblings DEPRECATED: Optional PageArray of siblings to use instead of default.
* @param PageArray|null $siblings Optional PageArray of siblings to use instead of default.
* @return PageArray
*
*/
public function prevUntil($selector = '', $filter = '', PageArray $siblings = null) {
if($siblings === null && $this->traversalPages) $siblings = $this->traversalPages;
if($siblings) return $this->traversal()->prevUntilSiblings($this, $selector, $filter, $siblings);
return $this->traversal()->prevUntil($this, $selector, $filter);
}
/**
* Return pages that have Page reference fields pointing to this one (references)
*
* By default this excludes pages that are hidden, unpublished and pages excluded due to access control for the current user.
* To prevent these exclusions specify an include mode in the selector, i.e. `include=all`, or you can use
* boolean `true` as a shortcut to specify that you do not want any exclusions.
*
* #pw-group-traversal
*
* @param string|bool $selector Optional selector to filter results by, or boolean true as shortcut for `include=all`.
* @param Field|string|bool $field Optionally limit to pages using specified field (name or Field object),
* - OR specify boolean TRUE to return array of PageArrays indexed by field names.
* - If $field argument not specified, it searches all applicable Page fields.
* @return PageArray|array
* @since 3.0.107
*
*/
public function ___references($selector = '', $field = '') {
return $this->traversal()->references($this, $selector, $field);
}
/**
* Return pages linking to this one (in Textarea/HTML fields)
*
* Applies only to Textarea fields with “html” content-type and link abstraction enabled.
*
* #pw-group-traversal
*
* @param string|bool $selector Optional selector to filter by or boolean true for “include=all”. (default='')
* @param string|Field $field Optionally limit results to specified field. (default=all applicable Textarea fields)
* @return PageArray
* @since 3.0.107
*
*/
public function ___links($selector = '', $field = '') {
return $this->traversal()->links($this, $selector, $field);
}
/**
* Get languages active for this page and viewable by current user
@@ -2362,24 +2574,30 @@ class Page extends WireData implements \Countable, WireMatchable {
*
*/
public function save($field = null, array $options = array()) {
if(is_array($field) && empty($options)) {
$options = $field;
$field = null;
}
if(!is_null($field)) {
if($this->hasField($field)) {
return $this->wire('pages')->saveField($this, $field, $options);
} else {
return false;
}
if(empty($field)) {
return $this->wire('pages')->save($this, $options);
}
if($this->hasField($field)) {
// save field
return $this->wire('pages')->saveField($this, $field, $options);
}
// save only native properties
$options['noFields'] = true;
return $this->wire('pages')->save($this, $options);
}
/**
* Quickly set field value(s) and save to database
*
* You can specify a single vield and value, or an array of fields and values.
* You can specify a single field and value, or an array of fields and values.
*
* This method does not need output formatting to be turned off first, so make sure that whatever
* value(s) you set are not formatted values.
@@ -2642,6 +2860,7 @@ class Page extends WireData implements \Countable, WireMatchable {
*
* #pw-hookable
* #pw-group-common
* #pw-group-urls
*
* ~~~~~
* // Difference between path and url on site running from subdirectory /my-site/
@@ -2767,6 +2986,9 @@ class Page extends WireData implements \Countable, WireMatchable {
* ]);
* ~~~~~
*
* #pw-group-common
* #pw-group-urls
*
* @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()
@@ -2779,6 +3001,37 @@ class Page extends WireData implements \Countable, WireMatchable {
return $url;
}
/**
* Return all URLs that this page can be accessed from (excluding URL segments and pagination)
*
* This includes the current page URL, any other language URLs (for which page is active), and
* any past (historical) URLs the page was previously available at (which will redirect to it).
*
* - Returned URLs do not include additional URL segments or pagination numbers.
* - Returned URLs are indexed by language name, i.e. “default”, “fr”, “es”, etc.
* - If multi-language URLs not installed, then index is just “default”.
* - Past URLs are indexed by language; then ISO-8601 date, i.e. “default;2016-08-11T07:44:43-04:00”,
* where the date represents the last date that URL was considered current.
* - If PagePathHistory core module is not installed then past/historical URLs are excluded.
* - You can disable past/historical or multi-language URLs by using the $options argument.
*
* #pw-group-urls
*
* @param array $options Options to modify default behavior:
* - `http` (bool): Make URLs include current scheme and hostname (default=false).
* - `past` (bool): Include past/historical URLs? (default=true)
* - `languages` (bool): Include other language URLs when supported/available? (default=true).
* - `language` (Language|int|string): Include only URLs for this language (default=null).
* Note: the `languages` option must be true if using the `language` option.
* @return array
* @since 3.0.107
* @see Page::addUrl(), page::removeUrl()
*
*/
public function urls($options = array()) {
return $this->traversal()->urls($this, $options);
}
/**
* Returns the URL to the page, including scheme and hostname
*
@@ -2794,6 +3047,9 @@ class Page extends WireData implements \Countable, WireMatchable {
* // Generating a link to this page using httpUrl
* echo "<a href='$page->httpUrl'>$page->title</a>";
* ~~~~~
*
* #pw-group-common
* #pw-group-urls
*
* @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/`
@@ -2801,15 +3057,23 @@ class Page extends WireData implements \Countable, WireMatchable {
*
*/
public function httpUrl($options = array()) {
if(!$this->template) return '';
switch($this->template->https) {
$template = $this->template;
if(!$template) return '';
/** @var Config $config */
$config = $this->wire('config');
$mode = $template->https;
if($mode > 0 && $config->noHTTPS) $mode = 0;
switch($mode) {
case -1: $protocol = 'http'; break;
case 1: $protocol = 'https'; break;
default: $protocol = $this->wire('config')->https ? 'https' : 'http';
default: $protocol = $config->https ? 'https' : 'http';
}
if(is_array($options)) unset($options['http']);
else if(is_bool($options)) $options = array();
return "$protocol://" . $this->wire('config')->httpHost . $this->url($options);
if(is_array($options)) {
unset($options['http']);
} else if(is_bool($options)) {
$options = array();
}
return "$protocol://" . $config->httpHost . $this->url($options);
}
/**
@@ -2825,23 +3089,38 @@ class Page extends WireData implements \Countable, WireMatchable {
* }
* ~~~~~~
*
* #pw-group-advanced
* #pw-group-urls
*
* @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).
* - `language` (Language|bool): Optionally specify Language to start editor in, or boolean true to force current user language.
* @return string URL for editing this page
*
*/
public function editUrl($options = array()) {
/** @var Config $config */
$config = $this->wire('config');
/** @var Template $adminTemplate */
$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";
$https = $adminTemplate && ($adminTemplate->https > 0) && !$config->noHTTPS;
$url = ($https && !$config->https) ? 'https://' . $config->httpHost : '';
$url .= $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;
$url = ($https ? 'https://' : 'http://') . $config->httpHost . $url;
}
}
if($this->wire('languages')) {
$language = $this->wire('user')->language;
if(empty($options['language'])) {
if($this->wire('page')->template->id == $adminTemplate->id) $language = null;
} else if($options['language'] instanceof Page) {
$language = $options['language'];
} else if($options['language'] !== true) {
$language = $this->wire('languages')->get($options['language']);
}
if($language && $language->id) $url .= "&language=$language->id";
}
$append = $this->wire('session')->getFor($this, 'appendEditUrl');
if($append) $url .= $append;
return $url;
@@ -2870,6 +3149,15 @@ class Page extends WireData implements \Countable, WireMatchable {
/**
* Return the index/position of this page relative to siblings.
*
* If given a hidden or unpublished page, that page would not usually be part of the group of siblings.
* As a result, such pages will return what the value would be if they were visible (as of 3.0.121). This
* may overlap with the index of other pages, since indexes are relative to visible pages, unless you
* specify an include mode (see next paragraph).
*
* If you want this method to include hidden/unpublished pages as part of the index numbers, then
* specify boolean true for the $selector argument (which implies "include=all") OR specify a
* selector of "include=hidden", "include=unpublished" or "include=all".
*
* ~~~~~
* $i = $page->index();
* $n = $page->parent->numChildren();
@@ -2878,12 +3166,16 @@ class Page extends WireData implements \Countable, WireMatchable {
*
* #pw-group-traversal
*
* @param bool|string|array Specify one of the following (since 3.0.121):
* - Boolean true to include hidden and unpublished pages as part of the index numbers (same as "include=all").
* - An "include=hidden", "include=unpublished" or "include=all" selector to include them in the index numbers.
* - A string selector or selector array to filter the criteria for the returned index number.
* @return int Returns index number (zero-based)
* @since 3.0.24
*
*/
public function index() {
return $this->traversal()->index($this);
public function index($selector = '') {
return $this->traversal()->index($this, $selector);
}
/**
@@ -3323,9 +3615,14 @@ class Page extends WireData implements \Countable, WireMatchable {
if(is_null($status)) $status = $this->status;
if($value === false) return $status;
$names = array();
$remainder = $status;
foreach(self::$statuses as $name => $value) {
if($status & $value) $names[$value] = $name;
if($status & $value) {
$names[$value] = $name;
$remainder = $remainder & ~$value;
}
}
if($remainder > 1) $names[$remainder] = "unknown-$remainder";
return $names;
}
@@ -3376,10 +3673,15 @@ class Page extends WireData implements \Countable, WireMatchable {
*
*/
protected function processFieldDataQueue() {
$template = $this->template;
if(!$template) return;
$fieldgroup = $template->fieldgroup;
if(!$fieldgroup) return;
foreach($this->fieldDataQueue as $key => $value) {
$field = $this->fieldgroup->get($key);
$field = $fieldgroup->get($key);
if(!$field) continue;
// check for autojoin multi fields, which may have multiple values bundled into one string
@@ -3514,6 +3816,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]);
}
}
}
@@ -3652,7 +3955,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* Return a Page helper class instance that's common among all Page objects in this ProcessWire instance
*
* @param $className
* @return object
* @return object|PageComparison|PageAccess|PageTraversal
*
*/
protected function getHelperInstance($className) {
@@ -3722,6 +4025,30 @@ class Page extends WireData implements \Countable, WireMatchable {
// $this->setQuietly('_editor', $editor); // uncomment when/if needed
}
/**
* Get or set current traversal pages (internal use)
*
* When setting, force use of given $items (siblings) and sort order in some
* traversal methods like next(), prev() and related methods. Given $items must
* include this page as well before used in any traversal calls.
*
* - To set, specify a PageArray for $items.
* - To unset, specify boolean false for $items.
* - To get current traversal pages omit all arguments.
*
* #pw-internal
*
* @param PageArray|bool|null $items Traversal pages (PageArray), boolean false to unset, or omit to get.
* @return PageArray|null
* @since 3.0.116
*
*/
public function traversalPages($items = null) {
if($items instanceof PageArray) $this->traversalPages = $items; // set
if($items === false) $this->traversalPages = null; // unset
return $this->traversalPages; // get
}
/**
* Get the icon name associated with this Page (if applicable)
*
@@ -3817,7 +4144,7 @@ class Page extends WireData implements \Countable, WireMatchable {
* @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
* @return Pages|Page|PageArray|NullPage|int
* @throws WireException
*
*/

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

@@ -108,6 +108,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*/
public function getItemKey($item) {
if(!$item instanceof Page) return null;
if(!$this->duplicateChecking) return parent::getItemKey($item);
// first see if we can determine key from our index
$id = $item->id;
@@ -284,7 +285,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) {
@@ -351,7 +352,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) {
@@ -368,7 +369,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) {
@@ -383,7 +384,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) {
@@ -453,7 +454,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) {
@@ -467,7 +468,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) {
@@ -480,7 +481,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) {
@@ -495,7 +496,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) {
@@ -567,7 +568,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
*
* #pw-internal
*
* @return Page[]|\ArrayObject
* @return Page[]|\ArrayObject|PageArrayIterator
*
*/
public function getIterator() {
@@ -628,7 +629,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

@@ -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;
*/
}
}

View File

@@ -1,8 +1,5 @@
<?php namespace ProcessWire;
class PageFinderException extends WireException { }
class PageFinderSyntaxException extends PageFinderException { }
/**
* ProcessWire PageFinder
*
@@ -13,10 +10,11 @@ class PageFinderSyntaxException extends PageFinderException { }
*
* Hookable methods:
* =================
* @method array|DatabaseQuerySelect find(Selectors $selectors, $options = array())
* @method array|DatabaseQuerySelect find(Selectors|string|array $selectors, $options = array())
* @method DatabaseQuerySelect getQuery($selectors, array $options)
* @method string getQueryAllowedTemplatesWhere(DatabaseQuerySelect $query, $where)
* @method void getQueryJoinPath(DatabaseQuerySelect $query, $selector)
* @method bool|Field getQueryUnknownField($fieldName, array $data);
*
*/
@@ -45,7 +43,7 @@ class PageFinder extends Wire {
*
*/
'findUnpublished' => false,
/**
* Specify that it's okay for hidden AND unpublished AND trashed pages to be included in the results
*
@@ -58,6 +56,12 @@ class PageFinder extends Wire {
*/
'findAll' => false,
/**
* Always allow these page IDs to be included regardless of findHidden, findUnpublished, findTrash, findAll settings
*
*/
'alwaysAllowIDs' => array(),
/**
* This is an optimization used by the Pages::find method, but we observe it here as we may be able
* to apply some additional optimizations in certain cases. For instance, if loadPages=false, then
@@ -158,6 +162,7 @@ class PageFinder extends Wire {
protected $extraOrSelectors = array(); // one from each field must match
protected $sortsAfter = array(); // apply these sorts after pages loaded
protected $reverseAfter = false; // reverse order after load?
protected $pageArrayData = array(); // any additional data that should be populated back to any resulting PageArray objects
// protected $extraSubSelectors = array(); // subselectors that are added in after getQuery()
// protected $extraJoins = array();
@@ -327,7 +332,7 @@ class PageFinder extends Wire {
/**
* Return all pages matching the given selector.
*
* @param Selectors|string|array $selectors Selectors object or selector string
* @param Selectors|string|array $selectors Selectors object, selector string or selector array
* @param array $options
* - `findOne` (bool): Specify that you only want to find 1 page and don't need info for pagination (default=false).
* - `findHidden` (bool): Specify that it's okay for hidden pages to be included in the results (default=false).
@@ -346,7 +351,7 @@ class PageFinder extends Wire {
* - `returnQuery` (bool): When true, only the DatabaseQuery object is returned by find(), for internal use. (default=false)
* - `loadPages` (bool): This is an optimization used by the Pages::find() method, but we observe it here as we
* may be able to apply some additional optimizations in certain cases. For instance, if loadPages=false, then
* we can skip retrieval of IDs and omit sort fields. (default=true)
* we can skip retrieval of IDs and omit sort fields. (default=true)
* - `stopBeforeID` (int): Stop loading pages once a page matching this ID is found. Page having this ID will be
* excluded as well (default=0).
* - `startAfterID` (int): Start loading pages once a page matching this ID is found. Page having this ID will be
@@ -366,7 +371,7 @@ class PageFinder extends Wire {
if(is_string($selectors) || is_array($selectors)) {
$selectors = new Selectors($selectors);
} else if(!$selectors instanceof Selectors) {
throw new PageFinderException("find() requires Selectors object or string");
throw new PageFinderException("find() requires Selectors object, string or array");
}
$this->fieldgroups = $this->wire('fieldgroups');
@@ -376,6 +381,7 @@ class PageFinder extends Wire {
$this->templates_id = null;
$this->checkAccess = true;
$this->getQueryNumChildren = 0;
$this->pageArrayData = array();
$this->setupStatusChecks($selectors, $options);
// move getTotal option to a class property, after setupStatusChecks
@@ -487,16 +493,48 @@ class PageFinder extends Wire {
/**
* Same as find() but returns just a simple array of page IDs without any other info
*
* @param Selectors $selectors
* @param Selectors|string|array $selectors Selectors object, selector string or selector array
* @param array $options
* @return array of page IDs
*
*/
public function findIDs(Selectors $selectors, $options = array()) {
public function findIDs($selectors, $options = array()) {
$options['returnVerbose'] = false;
return $this->find($selectors, $options);
}
/**
* Return a count of pages that match
*
* @param Selectors|string|array $selectors Selectors object, selector string or selector array
* @param array $options
* @return int
* @since 3.0.121
*
*/
public function count($selectors, $options = array()) {
$defaults = array(
'getTotal' => true,
'getTotalType' => 'count',
'loadPages' => false,
'returnVerbose' => false
);
$options = array_merge($defaults, $options);
if(!empty($options['startBeforeID']) || !empty($options['stopAfterID'])) {
$options['loadPages'] = true;
$options['getTotalType'] = 'calc';
$count = count($this->find($selectors, $options));
} else {
$this->find($selectors, $options);
$count = $this->total;
}
return $count;
}
/**
* Pre-process given Selectors object
*
@@ -513,12 +551,13 @@ class PageFinder extends Wire {
$eq = null;
foreach($selectors as $selector) {
$field = $selector->field;
$field = $selector->field();
if($field === '_custom') {
$selectors->remove($selector);
if(!empty($options['allowCustom'])) {
$_selectors = $this->wire(new Selectors($selector->value()));
$this->preProcessSelectors($_selectors, $options);
/** @var Selectors $_selectors */
foreach($_selectors as $s) $selectors->add($s);
}
@@ -546,6 +585,12 @@ class PageFinder extends Wire {
$eq = (int) $value;
}
$selectors->remove($selector);
} else if(strpos($field, '.owner.') && !$this->wire('fields')->get('owner')) {
$selector->field = str_replace('.owner.', '__owner.', $selector->field());
} else if(stripos($field, 'Fieldtype') === 0) {
$this->preProcessFieldtypeSelector($selectors, $selector);
}
}
@@ -610,6 +655,7 @@ class PageFinder extends Wire {
$o['getTotal'] = true;
$o['loadPages'] = false;
$o['returnVerbose'] = false;
/** @var Selectors $sel */
$sel = clone $selectors;
foreach($sel as $s) {
if($s->field == 'limit' || $s->field == 'start') $sel->remove($s);
@@ -646,6 +692,117 @@ class PageFinder extends Wire {
$this->start = $start;
}
/**
* Pre-process a selector having field name that begins with "Fieldtype"
*
* @param Selectors $selectors
* @param Selector $selector
*
*/
protected function preProcessFieldtypeSelector(Selectors $selectors, Selector $selector) {
$foundFields = null;
$foundTypes = null;
$replaceFields = array();
$failFields = array();
/** @var Languages|null $languages */
$languages = $this->wire('languages');
$selectorCopy = null;
foreach($selector->fields() as $fieldName) {
$subfield = '';
$findPerField = false;
$findExtends = false;
if(strpos($fieldName, '.')) {
$parts = explode('.', $fieldName);
$fieldName = array_shift($parts);
foreach($parts as $k => $part) {
if($part === 'fields') {
$findPerField = true;
unset($parts[$k]);
} else if($part === 'extends') {
$findExtends = true;
unset($parts[$k]);
}
}
if(count($parts)) $subfield = implode('.', $parts);
}
$fieldtype = $this->wire('fieldtypes')->get($fieldName);
if(!$fieldtype) continue;
$fieldtypeLang = $languages ? $this->wire('fieldtypes')->get("{$fieldName}Language") : null;
foreach($this->wire('fields') as $f) {
if($findExtends) {
// allow any Fieldtype that is an instance of given one, or extends it
if(!wireInstanceOf($f->type, $fieldtype)
&& ($fieldtypeLang === null || !wireInstanceOf($f->type, $fieldtypeLang))) continue;
} else {
// only allow given Fieldtype
if($f->type !== $fieldtype && ($fieldtypeLang === null || $f->type !== $fieldtypeLang)) continue;
}
$fName = $subfield ? "$f->name.$subfield" : $f->name;
if($findPerField) {
if($selectorCopy === null) $selectorCopy = clone $selector;
$selectorCopy->field = $fName;
$selectors->replace($selector, $selectorCopy);
$count = $this->wire('pages')->count($selectors);
$selectors->replace($selectorCopy, $selector);
if($count) {
if($foundFields === null) {
$foundFields = isset($this->pageArrayData['fields']) ? $this->pageArrayData['fields'] : array();
}
// include only fields that we know will match
$replaceFields[$fName] = $fName;
if(isset($foundFields[$fName])) {
$foundFields[$fName] += $count;
} else {
$foundFields[$fName] = $count;
}
} else {
$failFields[$fName] = $fName;
}
} else {
// include all fields (faster)
$replaceFields[$fName] = $fName;
}
if($findExtends) {
if($foundTypes === null) {
$foundTypes = isset($this->pageArrayData['extends']) ? $this->pageArrayData['extends'] : array();
}
$fType = $f->type->className();
if(isset($foundTypes[$fType])) {
$foundTypes[$fType][] = $fName;
} else {
$foundTypes[$fType] = array($fName);
}
}
}
}
if(count($replaceFields)) {
$selector->fields = array_values($replaceFields);
} else if(count($failFields)) {
// forced non-match and prevent field-not-found error after this method
$selector->field = reset($failFields);
}
if(is_array($foundFields)) {
arsort($foundFields);
$this->pageArrayData['fields'] = $foundFields;
}
if(is_array($foundTypes)) {
$this->pageArrayData['extends'] = $foundTypes;
}
}
/**
* Pre-process the given selector to perform any necessary replacements
@@ -662,19 +819,45 @@ class PageFinder extends Wire {
*
*/
protected function preProcessSelector(Selector $selector, Selectors $selectors, array $options, $level = 0) {
/** @var Fields $fields */
$fields = $this->wire('fields');
$quote = $selector->quote;
$fields = $selector->fields;
$fieldsArray = $selector->fields;
$hasDoubleDot = false;
$tags = null;
foreach($fields as $fn) {
foreach($fieldsArray as $key => $fn) {
$dot = strpos($fn, '.');
$parts = $dot ? explode('.', $fn) : array($fn);
// determine if it is a double-dot field (a.b.c)
if($dot && strrpos($fn, '.') !== $dot) {
if(strpos($fn, '__owner.') !== false) continue;
$hasDoubleDot = true;
break;
}
// determine if it is referencing any tags that should be coverted to field1|field2|field3
foreach($parts as $partKey => $part) {
if($tags !== null && empty($tags)) continue;
if($fields->get($part)) continue; // maps to Field object
if($fields->isNative($part)) continue; // maps to native property
if($tags === null) $tags = $fields->getTags(true); // determine tags
if(!isset($tags[$part])) continue; // not a tag
$tagFields = $tags[$part];
foreach($tagFields as $k => $fieldName) {
$_parts = $parts;
$_parts[$partKey] = $fieldName;
$tagFields[$k] = implode('.', $_parts);
}
if(count($tagFields)) {
unset($fieldsArray[$key]);
$selector->fields = array_merge($fieldsArray, $tagFields);
}
}
}
if($quote == '[') {
// selector contains another embedded selector that we need to convert to page IDs
// i.e. field=[id>0, name=something, this=that]
@@ -683,24 +866,37 @@ class PageFinder extends Wire {
} else if($quote == '(') {
// selector contains an OR group (quoted selector)
// at least one (quoted selector) must match for each field specified in front of it
$groupName = $this->wire('sanitizer')->fieldName($selector->getField('string'));
$groupName = $selector->group ? $selector->group : $selector->getField('string');
$groupName = $this->wire('sanitizer')->fieldName($groupName);
if(!$groupName) $groupName = 'none';
if(!isset($this->extraOrSelectors[$groupName])) $this->extraOrSelectors[$groupName] = array();
if($selector->value instanceof Selectors) {
$this->extraOrSelectors[$groupName][] = $selector->value;
} else {
$this->extraOrSelectors[$groupName][] = $this->wire(new Selectors($selector->value));
if($selector->group) {
// group is pre-identified, indicating Selector field=value is the OR-group condition
$s = clone $selector;
$s->quote = '';
$s->group = null;
$groupSelectors = new Selectors();
$groupSelectors->add($s);
} else {
// selector field is group name and selector value is another selector containing OR-group condition
$groupSelectors = new Selectors($selector->value);
}
$this->wire($groupSelectors);
$this->extraOrSelectors[$groupName][] = $groupSelectors;
}
return false;
} else if($hasDoubleDot) {
// has an "a.b.c" type string in the field, convert to a sub-selector
if(count($fields) > 1) {
if(count($fieldsArray) > 1) {
throw new PageFinderSyntaxException("Multi-dot 'a.b.c' type selectors may not be used with OR '|' fields");
}
$fn = reset($fields);
$fn = reset($fieldsArray);
$parts = explode('.', $fn);
$fieldName = array_shift($parts);
$field = $this->isPageField($fieldName);
@@ -823,7 +1019,7 @@ class PageFinder extends Wire {
}
}
if(!$hasParent && $field->parent_id) {
if(strpos($field->type->className(), 'FieldtypeRepeater') !== false) {
if($this->isRepeaterFieldtype($field->type)) {
// repeater items not stored directly under parent_id, but as another parent under parent_id.
// so we use has_parent instead here
$selectors->prepend(new SelectorEqual('has_parent', $field->parent_id));
@@ -916,6 +1112,7 @@ class PageFinder extends Wire {
$fieldCnt = array(); // counts number of instances for each field to ensure unique table aliases for ANDs on the same field
$lastSelector = null;
$sortSelectors = array(); // selector containing 'sort=', which gets added last
$subqueries = array();
$joins = array();
// $this->extraJoins = array();
$database = $this->wire('database');
@@ -971,7 +1168,7 @@ class PageFinder extends Wire {
continue;
} else if($this->wire('fields')->isNative($field) || strpos($fieldsStr, ':parent.') !== false) {
$this->getQueryNativeField($query, $selector, $fields);
$this->getQueryNativeField($query, $selector, $fields, $options);
continue;
}
@@ -984,37 +1181,60 @@ class PageFinder extends Wire {
foreach($fields as $n => $fieldName) {
// if a specific DB field from the table has been specified, then get it, otherwise assume 'data'
if(strpos($fieldName, ".")) list($fieldName, $subfield) = explode(".", $fieldName);
else $subfield = 'data';
if(strpos($fieldName, '.')) {
// if fieldName is "a.b.c" $subfields (plural) retains "b.c" while $subfield is just "b"
list($fieldName, $subfields) = explode('.', $fieldName, 2);
if(strpos($subfields, '.')) {
list($subfield) = explode('.', $subfields); // just the first
} else {
$subfield = $subfields;
}
} else {
$subfields = 'data';
$subfield = 'data';
}
$field = $this->wire('fields')->get($fieldName);
if(!$field = $this->wire('fields')->get($fieldName)) {
// field does not exist, try to match an API variable
$value = $this->wire($fieldName);
if(!$value) throw new PageFinderSyntaxException("Field does not exist: $fieldName");
if(count($fields) > 1) throw new PageFinderSyntaxException("You may only match 1 API variable at a time");
if(is_object($value)) {
if($subfield == 'data') $subfield = 'id';
$selector->field = $subfield;
if(!$field) {
// field does not exist, see if it can be processed in some other way
$field = $this->getQueryUnknownField($fieldName, array(
'subfield' => $subfield,
'subfields' => $subfields,
'fields' => $fields,
'query' => $query,
'selector' => $selector,
'selectors' => $selectors
));
if($field === true) {
// true indicates the hook modified query to handle this (or ignore it), and should move to next field
continue;
} else if($field instanceof Field) {
// hook has mapped it to a field and processing of field should proceed
} else if($field) {
// mapped it to an API var or something else where we need not continue processing $field or $fields
break;
} else {
throw new PageFinderSyntaxException("Field does not exist: $fieldName");
}
if(!$selector->matches($value)) {
$query->where("1>2"); // force non match
}
break;
}
// keep track of number of times this table name has appeared in the query
if(!isset($fieldCnt[$field->table])) $fieldCnt[$field->table] = 0;
else $fieldCnt[$field->table]++;
if(isset($fieldCnt[$field->table])) {
$fieldCnt[$field->table]++;
} else {
$fieldCnt[$field->table] = 0;
}
// use actual table name if first instance, if second instance of table then add a number at the end
$tableAlias = $field->table . ($fieldCnt[$field->table] ? $fieldCnt[$field->table] : '');
$tableAlias = $database->escapeTable($tableAlias);
$valueArray = is_array($selector->value) ? $selector->value : array($selector->value);
$join = '';
$numEmptyValues = 0;
$valueArray = $selector->values(true);
$fieldtype = $field->type;
$operator = $selector->operator;
$numEmptyValues = 0;
foreach($valueArray as $value) {
@@ -1033,8 +1253,11 @@ class PageFinder extends Wire {
}
/** @var DatabaseQuerySelect $q */
if(isset($subqueries[$tableAlias])) $q = $subqueries[$tableAlias];
else $q = $this->wire(new DatabaseQuerySelect());
if(isset($subqueries[$tableAlias])) {
$q = $subqueries[$tableAlias];
} else {
$q = $this->wire(new DatabaseQuerySelect());
}
$q->set('field', $field); // original field if required by the fieldtype
$q->set('group', $group); // original group of the field, if required by the fieldtype
@@ -1078,7 +1301,7 @@ class PageFinder extends Wire {
if(count($fields) > 1
|| (count($valueArray) > 1 && $numEmptyValues > 0)
|| $subfield == 'count'
|| ($subfield == 'count' && !$this->isRepeaterFieldtype($field->type))
|| ($selector->not && $selector->operator != '!=')
|| $selector->operator == '!=') {
// join should instead be a leftjoin
@@ -1116,8 +1339,11 @@ class PageFinder extends Wire {
} // fields
if(strlen($whereFields)) {
if(strlen($where)) $where = "($where) $whereFieldsType ($whereFields)";
else $where .= "($whereFields)";
if(strlen($where)) {
$where = "($where) $whereFieldsType ($whereFields)";
} else {
$where .= "($whereFields)";
}
}
} // selectors
@@ -1125,7 +1351,9 @@ class PageFinder extends Wire {
if($where) {
$query->where("($where)", $whereBindValues);
} else if(count($whereBindValues)) {
foreach($whereBindValues as $k => $v) $query->bindValue($k, $v);
foreach($whereBindValues as $k => $v) {
$query->bindValue($k, $v);
}
}
$this->getQueryAllowedTemplates($query, $options);
@@ -1136,17 +1364,15 @@ class PageFinder extends Wire {
$query->$joinType("$j[table] AS $j[tableAlias] ON $j[tableAlias].pages_id=pages.id AND ($j[join])");
}
/*
foreach($this->extraJoins as $j) {
$query->join($j);
if(count($sortSelectors)) {
foreach(array_reverse($sortSelectors) as $s) {
$this->getQuerySortSelector($query, $s);
}
}
*/
if(count($sortSelectors)) foreach(array_reverse($sortSelectors) as $s) $this->getQuerySortSelector($query, $s);
$this->postProcessQuery($query);
return $query;
}
/**
@@ -1574,8 +1800,13 @@ class PageFinder extends Wire {
$query->leftjoin("$table AS $tableAlias ON $tableAlias.pages_id=pages.$idColumn");
if($subValue === 'count') {
// sort by quantity of items
$value = "COUNT($tableAlias.data)";
if($this->isRepeaterFieldtype($field->type)) {
// repeaters have a native count column that can be used for sorting
$value = "$tableAlias.count";
} else {
// sort by quantity of items
$value = "COUNT($tableAlias.data)";
}
} else if(is_object($blankValue) && ($blankValue instanceof PageArray || $blankValue instanceof Page)) {
// If it's a FieldtypePage, then data isn't worth sorting on because it just contains an ID to the page
@@ -1679,6 +1910,7 @@ class PageFinder extends Wire {
foreach($this->wire('languages') as $language) {
if(!$language->isDefault()) $langNames[$language->id] = "name" . (int) $language->id;
}
if(!count($langNames)) $langNames = null;
} else {
$langNames = null;
}
@@ -1700,6 +1932,14 @@ class PageFinder extends Wire {
$query->where("pages.id=1");
} else {
$selectorValue = $selector->value;
if(is_array($selectorValue)) {
// only the PagePaths module can perform OR value searches on path/url
if($langNames) {
throw new PageFinderSyntaxException("OR values not supported for multi-language 'path' or 'url'");
} else {
throw new PageFinderSyntaxException("OR value support of 'path' or 'url' requires core PagePaths module");
}
}
if($langNames) $selectorValue = $this->wire('modules')->get('LanguageSupportPageNames')->updatePath($selectorValue);
$parts = explode('/', rtrim($selectorValue, '/'));
$part = $database->escapeStr($this->wire('sanitizer')->pageName(array_pop($parts), Sanitizer::toAscii));
@@ -1728,7 +1968,7 @@ class PageFinder extends Wire {
$query->join($sql);
} else {
$query->join("pages AS rootparent ON ($alias.parent_id=rootparent.id AND rootparent.id=1)");
$query->join("pages AS rootparent$n ON ($alias.parent_id=rootparent$n.id AND rootparent$n.id=1)");
}
$lastAlias = $alias;
}
@@ -1742,13 +1982,13 @@ class PageFinder extends Wire {
* @param DatabaseQuerySelect $query
* @param Selector $selector
* @param array $fields
* @param array $options
* @throws PageFinderSyntaxException
*
*/
protected function getQueryNativeField(DatabaseQuerySelect $query, $selector, $fields) {
protected function getQueryNativeField(DatabaseQuerySelect $query, $selector, $fields, array $options) {
$value = $selector->value;
$values = is_array($value) ? $value : array($value);
$values = $selector->values(true);
$SQL = '';
$database = $this->wire('database');
@@ -1788,7 +2028,7 @@ class PageFinder extends Wire {
}
$field = 'parent_id';
if(count($values) == 1 && $selector->getOperator() === '=') {
if(count($values) == 1 && $selector->operator() === '=') {
$this->parent_id = reset($values);
}
@@ -1837,7 +2077,7 @@ class PageFinder extends Wire {
// convert templates specified as a name to the numeric template ID
// allows selectors like 'template=my_template_name'
$field = 'templates_id';
if(count($values) == 1 && $selector->getOperator() === '=') $this->templates_id = reset($values);
if(count($values) == 1 && $selector->operator() === '=') $this->templates_id = reset($values);
if(!ctype_digit("$value")) $value = (($template = $this->wire('templates')->get($value)) ? $template->id : 0);
}
@@ -1876,6 +2116,14 @@ class PageFinder extends Wire {
if($isName) $value = $this->wire('sanitizer')->pageName($value, Sanitizer::toAscii);
$value = $database->escapeStr($value);
$s = "$table." . $field . $operator . ((ctype_digit("$value") && $field != 'name') ? ((int) $value) : "'$value'");
if($field === 'status' && strpos($operator, '<') === 0 && $value >= Page::statusHidden && count($options['alwaysAllowIDs'])) {
// support the 'alwaysAllowIDs' option for specific page IDs when requested but would
// not otherwise appear in the results due to hidden or unpublished status
$allowIDs = array();
foreach($options['alwaysAllowIDs'] as $id) $allowIDs[] = (int) $id;
$s = "($s OR $table.id IN(" . implode(',', $allowIDs) . '))';
}
}
if($selector->not) $s = "NOT ($s)";
@@ -2187,12 +2435,11 @@ class PageFinder extends Wire {
}
if($field) {
$className = $field->type->className();
if($field->type instanceof FieldtypePage) {
$is = true;
} else if(strpos($field->type->className(), 'FieldtypePageTable') !== false) {
$is = true;
} else if(strpos($className, 'FieldtypeRepeater') !== false) {
} else if($this->isRepeaterFieldtype($field->type)) {
$is = $literal ? false : true;
} else {
$test = $field->type->getBlankValue(new NullPage(), $field);
@@ -2204,5 +2451,220 @@ class PageFinder extends Wire {
if($is && $field) $is = $field;
return $is;
}
/**
* Is the given Fieldtype for a repeater?
*
* @param Fieldtype $fieldtype
* @return bool
*
*/
protected function isRepeaterFieldtype(Fieldtype $fieldtype) {
return wireInstanceOf($fieldtype, 'FieldtypeRepeater');
}
/**
* Hook called when an unknown field is found in the selector
*
* By default, PW will throw a PageFinderSyntaxException but that behavior can be overridden by
* hooking this method and making it return true rather than false. It may also choose to
* map it to a Field by returning a Field object. If it returns integer 1 then it indicates the
* fieldName mapped to an API variable. If this method returns false, then it signals the getQuery()
* method that it was unable to map it to anything and should be considered a fail.
*
* @param string $fieldName
* @param array $data Array of data containing the following in it:
* - `subfield` (string): First subfield
* - `subfields` (string): All subfields separated by period (i.e. subfield.tertiaryfield)
* - `fields` (array): Array of all other field names being processed in this selector.
* - `query` (DatabaseQuerySelect): Database query select object
* - `selector` (Selector): Selector that contains this field
* - `selectors` (Selectors): All the selectors
* @return bool|Field|int
* @throws PageFinderSyntaxException
*
*/
protected function ___getQueryUnknownField($fieldName, array $data) {
$_data = array(
'subfield ' => 'data',
'subfields' => 'data',
'fields' => array(),
'query' => null,
'selector' => null,
'selectors' => null,
);
$data = array_merge($_data, $data);
/** @var array $fields */
$fields = $data['fields'];
/** @var string $subfields */
$subfields = $data['subfields'];
/** @var Selector $selector */
$selector = $data['selector'];
/** @var DatabaseQuerySelect $query */
$query = $data['query'];
/** @var Wire|null $value */
$value = $this->wire($fieldName);
if($value) {
// found an API var
if(count($fields) > 1) {
throw new PageFinderSyntaxException("You may only match 1 API variable at a time");
}
if(is_object($value)) {
if($subfields == 'data') $subfields = 'id';
$selector->field = $subfields;
}
if(!$selector->matches($value)) {
$query->where("1>2"); // force non match
}
return 1; // indicate no further fields need processing
}
// not an API var
if($this->getQueryOwnerField($fieldName, $data)) return true;
return false;
}
/**
* Process an owner back reference selector for PageTable, Page and Repeater fields
*
* @param string $fieldName Field name in "fieldName__owner" format
* @param array $data Data as provided to getQueryUnknownField method
* @return bool True if $fieldName was processed, false if not
* @throws PageFinderSyntaxException
*
*/
protected function getQueryOwnerField($fieldName, array $data) {
if(substr($fieldName, -7) !== '__owner') return false;
/** @var array $fields */
$fields = $data['fields'];
/** @var string $subfields */
$subfields = $data['subfields'];
/** @var Selectors $selectors */
$selectors = $data['selectors'];
/** @var Selector $selector */
$selector = $data['selector'];
/** @var DatabaseQuerySelect $query */
$query = $data['query'];
if(empty($subfields)) throw new PageFinderSyntaxException("When using owner a subfield is required");
list($ownerFieldName,) = explode('__owner', $fieldName);
$ownerField = $this->wire('fields')->get($ownerFieldName);
if(!$ownerField) return false;
$ownerTypes = array('FieldtypeRepeater', 'FieldtypePageTable', 'FieldtypePage');
if(!wireInstanceOf($ownerField->type, $ownerTypes)) return false;
if($selector->get('owner_processed')) return true;
static $ownerNum = 0;
$ownerNum++;
// determine which templates are using $ownerFieldName
$templateIDs = array();
foreach($this->wire('templates') as $template) {
if($template->hasField($ownerFieldName)) {
$templateIDs[$template->id] = $template->id;
}
}
if(!count($templateIDs)) $templateIDs[] = 0;
$templateIDs = implode('|', $templateIDs);
// determine include=mode
$include = $selectors->getSelectorByField("include");
$include = $include ? $include->value : 'hidden';
/** @var Selectors $ownerSelectors Build selectors */
$ownerSelectors = $this->wire(new Selectors("templates_id=$templateIDs, include=$include, get_total=0"));
$ownerSelector = clone $selector;
if(count($fields) > 1) {
// OR fields present
array_shift($fields);
$subfields = array($subfields);
foreach($fields as $name) {
if(strpos($name, "$fieldName.") === 0) {
list(,$name) = explode('__owner.', $name);
$subfields[] = $name;
} else {
throw new PageFinderSyntaxException(
"When owner is present, group of OR fields must all be '$ownerFieldName.owner.subfield' format"
);
}
}
}
$ownerSelector->field = $subfields;
$ownerSelectors->add($ownerSelector);
// use field.count>0 as an optimization?
$useCount = true;
// find any other selectors referring to this same owner, bundle them in, and remove from source
foreach($selectors as $sel) {
if(strpos($sel->field(), "$fieldName.") !== 0) continue;
$sel->set('owner_processed', true);
$op = $sel->operator();
if($useCount && ($sel->not || strpos($op, '!') !== false || strpos($op, '<') !== false)) {
$useCount = false;
}
if($sel === $selector) {
continue; // skip main
}
$s = clone $sel;
$s->field = str_replace("$fieldName.", '', $sel->field());
$ownerSelectors->add($s);
$selectors->remove($sel);
}
if($useCount) {
$sel = new SelectorGreaterThan("$ownerFieldName.count", 0);
$ownerSelectors->add($sel);
}
/** @var PageFinder $finder */
$finder = $this->wire(new PageFinder());
$ids = $finder->findIDs($ownerSelectors);
if($this->isRepeaterFieldtype($ownerField->type)) {
// Repeater
$alias = "owner_parent$ownerNum";
$names = array();
foreach($ids as $id) {
$names[] = "'for-page-$id'";
}
$names = empty($names) ? "'force no match'" : implode(",", $names);
$query->join("pages AS $alias ON $alias.id=pages.parent_id AND $alias.name IN($names)");
} else {
// Page or PageTable
$table = $ownerField->getTable();
$alias = "owner{$ownerNum}_$table";
$ids = empty($ids) ? "0" : implode(',', $ids);
$query->join("$table AS $alias ON $alias.data=pages.id AND $alias.pages_id IN($ids)");
}
return true;
}
/**
* Get data that should be populated back to any resulting PageArrays data() method
*
* @param PageArray|null $pageArray Optionally populate given PageArray
* @return array
*
*/
public function getPageArrayData(PageArray $pageArray = null) {
if($pageArray !== null && count($this->pageArrayData)) {
$pageArray->data($this->pageArrayData);
}
return $this->pageArrayData;
}
}

View File

@@ -22,43 +22,87 @@ class PageTraversal {
* @param Page $page
* @param bool|string|int|array $selector
* When not specified, result includes all children without conditions, same as $page->numChildren property.
* When a string or array, a selector is assumed and quantity will be counted based on selector.
* When a string or array, a selector is assumed and quantity will be counted based on selector.
* When boolean true, number includes only visible children (excludes unpublished, hidden, no-access, etc.)
* When boolean false, number includes all children without conditions, including unpublished, hidden, no-access, etc.
* When integer 1 number includes viewable children (as opposed to visible, viewable includes hidden pages + it also includes unpublished pages if user has page-edit permission).
* @param array $options
* - `descendants` (bool): Use descendants rather than direct children
* @return int Number of children
*
*/
public function numChildren(Page $page, $selector = null) {
public function numChildren(Page $page, $selector = null, array $options = array()) {
$descendants = empty($options['descendants']) ? false : true;
$parentType = $descendants ? 'has_parent' : 'parent_id';
if(is_bool($selector)) {
// onlyVisible takes the place of selector
$onlyVisible = $selector;
if(!$onlyVisible) return $page->get('numChildren');
return $page->_pages('count', "parent_id=$page->id");
$numChildren = $page->get('numChildren');
if(!$numChildren) {
return 0;
} else if($onlyVisible) {
return $page->_pages('count', "$parentType=$page->id");
} else if($descendants) {
return $this->numDescendants($page);
} else {
return $numChildren;
}
} 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->_pages('count', "parent_id=$page->id, include=unpublished");
if($page->wire('user')->isSuperuser()) {
if($descendants) return $this->numDescendants($page);
return $numChildren;
} else if($page->wire('user')->hasPermission('page-edit')) {
return $page->_pages('count', "$parentType=$page->id, include=unpublished");
} else {
return $page->_pages('count', "$parentType=$page->id, include=hidden");
}
return $page->_pages('count', "parent_id=$page->id, include=hidden");
} else if(empty($selector) || (!is_string($selector) && !is_array($selector))) {
return $page->get('numChildren');
// no selector provided
if($descendants) return $this->numDescendants($page);
return $page->get('numChildren');
} else {
// selector string or array provided
if(is_string($selector)) {
$selector = "parent_id=$page->id, $selector";
$selector = "$parentType=$page->id, $selector";
} else if(is_array($selector)) {
$selector["parent_id"] = $page->id;
$selector[$parentType] = $page->id;
}
return $page->_pages('count', $selector);
}
}
/**
* Return number of descendants, optionally with conditions
*
* Use this over $page->numDescendants property when you want to specify a selector or when you want the result to
* include only visible descendants. See the options for the $selector argument.
*
* @param Page $page
* @param bool|string|int|array $selector
* When not specified, result includes all descendants without conditions, same as $page->numDescendants property.
* When a string or array, a selector is assumed and quantity will be counted based on selector.
* When boolean true, number includes only visible descendants (excludes unpublished, hidden, no-access, etc.)
* When boolean false, number includes all descendants without conditions, including unpublished, hidden, no-access, etc.
* When integer 1 number includes viewable descendants (as opposed to visible, viewable includes hidden pages + it also includes unpublished pages if user has page-edit permission).
* @return int Number of descendants
*
*/
public function numDescendants(Page $page, $selector = null) {
if($selector === null) {
return $page->_pages('count', "has_parent=$page->id, include=all");
} else {
return $this->numChildren($page, $selector, array('descendants' => true));
}
}
/**
* Return this page's children pages, optionally filtered by a selector
*
@@ -130,6 +174,28 @@ class PageTraversal {
return strlen($selector) ? $parents->filter($selector) : $parents;
}
/**
* Return number of parents (depth relative to homepage) that this page has, optionally filtered by a selector
*
* For example, homepage has 0 parents and root level pages have 1 parent (which is the homepage), and the
* number increases the deeper the page is in the pages structure.
*
* @param Page $page
* @param string $selector Optional selector to filter by (default='')
* @return int Number of parents
*
*/
public function numParents(Page $page, $selector = '') {
$num = 0;
$parent = $page->parent();
while($parent && $parent->id) {
if($selector !== '' && !$parent->matches($selector)) continue;
$num++;
$parent = $parent->parent();
}
return $num;
}
/**
* Return all parent from current till the one matched by $selector
*
@@ -216,11 +282,26 @@ class PageTraversal {
return $page->_pages('find', $selector, $options);
}
/**
* Get include mode specified in selector or blank if none
*
* @param string|array|Selectors $selector
* @return string
*
*/
protected function _getIncludeMode($selector) {
if(is_string($selector) && strpos($selector, 'include=') === false) return '';
if(is_array($selector)) return isset($selector['include']) ? $selector['include'] : '';
$selector = $selector instanceof Selectors ? $selector : new Selectors($selector);
$include = $selector->getSelectorByField('include');
return $include ? $include->value() : '';
}
/**
* Builds the PageFinder options for the _next() method
*
* @param Page $page
* @param string|array $selector
* @param string|array|Selectors $selector
* @param array $options
* @return array
*
@@ -232,7 +313,16 @@ class PageTraversal {
'startAfterID' => $options['prev'] ? 0 : $page->id,
'stopBeforeID' => $options['prev'] ? $page->id : 0,
'returnVerbose' => $options['all'] ? false : true,
'alwaysAllowIDs' => array(),
);
if($page->isUnpublished() || $page->isHidden()) {
// allow next() to still move forward even though it is hidden or unpublished
$includeMode = $this->_getIncludeMode($selector);
if(!$includeMode || ($includeMode === 'hidden' && $page->isUnpublished())) {
$fo['alwaysAllowIDs'][] = $page->id;
}
}
if(!$options['until']) return $fo;
@@ -280,7 +370,7 @@ class PageTraversal {
* Provides the core logic for next, prev, nextAll, prevAll, nextUntil, prevUntil
*
* @param Page $page
* @param string|array $selector Optional selector. When specified, will find nearest sibling(s) that match.
* @param string|array|Selectors $selector Optional selector. When specified, will find nearest sibling(s) that match.
* @param array $options Options to modify behavior
* - `prev` (bool): When true, previous siblings will be returned rather than next siblings.
* - `all` (bool): If true, returns all nextAll or prevAll rather than just single sibling (default=false).
@@ -305,12 +395,19 @@ class PageTraversal {
$parent = $page->parent();
if($options['until'] || $options['qty']) $options['all'] = true;
if(!$parent || !$parent->id) return $options['all'] ? $pages->newPageArray() : $pages->newNullPage();
if(!$parent || !$parent->id) {
if($options['qty']) return 0;
return $options['all'] ? $pages->newPageArray() : $pages->newNullPage();
}
if(is_array($selector)) {
$selector['parent_id'] = $parent->id;
} else {
} else if(is_string($selector)) {
$selector = trim("parent_id=$parent->id, $selector", ", ");
} else if($selector instanceof Selectors) {
$selector->add(new SelectorEqual('parent_id', $parent->id));
} else {
throw new WireException('Selector must be string, array or Selectors object');
}
$pageFinder = $pages->getPageFinder();
@@ -346,12 +443,26 @@ class PageTraversal {
/**
* Return the index/position of the given page relative to its siblings
*
* If given a hidden or unpublished page, that page would not usually be part of the group of siblings.
* As a result, such pages will return what the value would be if they were visible (as of 3.0.121). This
* may overlap with the index of other pages, since indexes are relative to visible pages, unless you
* specify an include mode (see next paragraph).
*
* If you want this method to include hidden/unpublished pages as part of the index numbers, then
* specify boolean true for the $selector argument (which implies "include=all") OR specify a
* selector of "include=hidden", "include=unpublished" or "include=all".
*
* @param Page $page
* @return int|NullPage|Page|PageArray
* @param string|array|bool|Selectors $selector Selector to apply or boolean true for "include=all" (since 3.0.121).
* - Boolean true to include hidden and unpublished pages as part of the index numbers (same as "include=all").
* - An "include=hidden", "include=unpublished" or "include=all" selector to include them in the index numbers.
* - A string selector or selector array to filter the criteria for the returned index number.
* @return int Returns index number (zero-based)
*
*/
public function index(Page $page) {
$index = $this->_next($page, '', array('prev' => true, 'all' => true, 'qty' => true));
public function index(Page $page, $selector = '') {
if($selector === true) $selector = "include=all";
$index = $this->_next($page, $selector, array('prev' => true, 'all' => true, 'qty' => 'index'));
return $index;
}
@@ -359,7 +470,7 @@ class PageTraversal {
* Return the next sibling page
*
* @param Page $page
* @param string $selector Optional selector. When specified, will find nearest next sibling that matches.
* @param string|array|Selectors $selector Optional selector. When specified, will find nearest next sibling that matches.
* @return Page|NullPage Returns the next sibling page, or a NullPage if none found.
*
*/
@@ -371,7 +482,7 @@ class PageTraversal {
* Return the previous sibling page
*
* @param Page $page
* @param string $selector Optional selector. When specified, will find nearest previous sibling that matches.
* @param string|array|Selectors $selector Optional selector. When specified, will find nearest previous sibling that matches.
* @return Page|NullPage Returns the previous sibling page, or a NullPage if none found.
*
*/
@@ -384,9 +495,9 @@ class PageTraversal {
* Return all sibling pages after this one, optionally matching a selector
*
* @param Page $page
* @param string $selector Optional selector. When specified, will filter the found siblings.
* @param string|array|Selectors $selector Optional selector. When specified, will filter the found siblings.
* @param array $options Options to pass to the _next() method
* @return Page|NullPage Returns all matching pages after this one.
* @return PageArray Returns all matching pages after this one.
*
*/
public function nextAll(Page $page, $selector = '', array $options = array()) {
@@ -399,9 +510,9 @@ class PageTraversal {
* Return all sibling pages prior to this one, optionally matching a selector
*
* @param Page $page
* @param string $selector Optional selector. When specified, will filter the found siblings.
* @param string|array|Selectors $selector Optional selector. When specified, will filter the found siblings.
* @param array $options Options to pass to the _next() method
* @return Page|NullPage Returns all matching pages after this one.
* @return PageArray Returns all matching pages after this one.
*
*/
public function prevAll(Page $page, $selector = '', array $options = array()) {
@@ -417,7 +528,7 @@ class PageTraversal {
* Return all sibling pages after this one until matching the one specified
*
* @param Page $page
* @param string|Page|array $selector May either be a selector or Page to stop at. Results will not include this.
* @param string|Page|array|Selectors $selector May either be a selector or Page to stop at. Results will not include this.
* @param string|array $filter Optional selector to filter matched pages by
* @param array $options Options to pass to the _next() method
* @return PageArray
@@ -576,7 +687,263 @@ class PageTraversal {
return $url;
}
/**
* Return all URLs that this page can be accessed from (excluding URL segments and pagination)
*
* This includes the current page URL, any other language URLs (for which page is active), and
* any past (historical) URLs the page was previously available at (which will redirect to it).
*
* - Returned URLs do not include additional URL segments or pagination numbers.
* - Returned URLs are indexed by language name, i.e. “default”, “fr”, “es”, etc.
* - If multi-language URLs not installed, then index is just “default”.
* - Past URLs are indexed by language; then ISO-8601 date, i.e. “default;2016-08-11T07:44:43-04:00”,
* where the date represents the last date that URL was considered current.
* - If PagePathHistory core module is not installed then past/historical URLs are excluded.
* - You can disable past/historical or multi-language URLs by using the $options argument.
*
* @param Page $page
* @param array $options Options to modify default behavior:
* - `http` (bool): Make URLs include current scheme and hostname (default=false).
* - `past` (bool): Include past/historical URLs? (default=true)
* - `languages` (bool): Include other language URLs when supported/available? (default=true).
* - `language` (Language|int|string): Include only URLs for this language (default=null).
* Note: the `languages` option must be true if using the `language` option.
* @return array
*
*/
public function urls(Page $page, $options = array()) {
$defaults = array(
'http' => false,
'past' => true,
'languages' => true,
'language' => null,
);
/** @var Modules $modules */
$modules = $page->wire('modules');
$options = array_merge($defaults, $options);
$languages = $options['languages'] ? $page->wire('languages') : null;
$slashUrls = $page->template->slashUrls;
$httpHostUrl = $options['http'] ? $page->wire('input')->httpHostUrl() : '';
$urls = array();
if($options['language'] && $languages) {
if(!$options['language'] instanceof Page) {
$options['language'] = $languages->get($options['language']);
}
if($options['language'] && $options['language']->id) {
$languages = array($options['language']);
}
}
// include other language URLs
if($languages && $modules->isInstalled('LanguageSupportPageNames')) {
foreach($languages as $language) {
if(!$language->isDefault() && !$page->get("status$language")) continue;
$urls[$language->name] = $page->localUrl($language);
}
} else {
$urls = array('default' => $page->url());
}
// add in historical URLs
if($options['past'] && $modules->isInstalled('PagePathHistory')) {
/** @var PagePathHistory $history */
$history = $modules->get('PagePathHistory');
$rootUrl = $page->wire('config')->urls->root;
$pastPaths = $history->getPathHistory($page, array(
'language' => $options['language'],
'verbose' => true
));
foreach($pastPaths as $pathInfo) {
$key = '';
if(!empty($pathInfo['language'])) {
/** @var Language $language */
$language = $pathInfo['language'];
if($options['languages']) {
$key .= $language->name . ';';
} else {
// they asked to have multi-language excluded
if(!$language->isDefault()) continue;
}
}
$key .= wireDate('c', $pathInfo['date']);
$urls[$key] = $rootUrl . ltrim($pathInfo['path'], '/');
}
}
// update URLs for current expected slash and http settings
foreach($urls as $key => $url) {
if($url !== '/') $url = $slashUrls ? rtrim($url, '/') . '/' : rtrim($url, '/');
if($options['http']) $url = $httpHostUrl . $url;
$urls[$key] = $url;
}
return $urls;
}
/**
* Return pages that are referencing the given one by way of Page references
*
* @param Page $page
* @param string|bool $selector Optional selector to filter results by or boolean true as shortcut for `include=all`.
* @param Field|string $field Limit to follower pages using this field,
* - or specify boolean TRUE to make it return array of PageArrays indexed by field name.
* @param bool $getCount Specify true to return counts rather than PageArray(s)
* @return PageArray|array|int
* @throws WireException Highly unlikely
*
*/
public function references(Page $page, $selector = '', $field = '', $getCount = false) {
$fieldtype = $page->wire('fieldtypes')->get('FieldtypePage');
if(!$fieldtype) throw new WireException('Unable to find FieldtypePage');
if($selector === true) $selector = "include=all";
return $fieldtype->findReferences($page, $selector, $field, $getCount);
}
/**
* Return number of VISIBLE pages that are following (referencing) the given one by way of Page references
*
* Note that this excludes hidden, unpublished and otherwise non-accessible pages (access control).
* If you do not want to exclude these, use the numFollowers() function instead, OR specify "include=all" for
* the $selector argument.
*
* @param Page $page
* @param string $selector Filter count by this selector
* @param string|Field|bool $field Limit count to given Field or specify boolean true to return array of counts.
* @return int|array Returns count, or array of counts (if $field==true)
*
*/
public function hasReferences(Page $page, $selector = '', $field = '') {
return $this->references($page, $selector, $field, true);
}
/**
* Return number of ANY pages that are following (referencing) the given one by way of Page references
*
* @param Page $page
* @param string $selector Filter count by this selector
* @param string|Field|bool $field Limit count to given Field or specify boolean true to return array of counts.
* @return int|array Returns count, or array of counts (if $field==true)
*
*/
public function numReferences(Page $page, $selector = '', $field = '') {
if(stripos($selector, "include=") === false) $selector = rtrim("include=all, $selector", ', ');
return $this->hasReferences($page, $selector, $field);
}
/**
* Return pages that this page is referencing by way of Page reference fields
*
* @param Page $page
* @param bool $field Limit results to requested field, or specify boolean true to return array indexed by field names.
* @param bool $getCount Specify true to return count(s) rather than pages.
* @return PageArray|int|array
*
*/
public function referencing(Page $page, $field = false, $getCount = false) {
$fieldName = '';
$byField = null;
if(is_bool($field) || is_null($field)) {
$byField = $field ? true : false;
} else if(is_string($field)) {
$fieldName = $page->wire('sanitizer')->fieldName($field);
} else if(is_int($field)) {
$field = $page->wire('fields')->get($field);
if($field) $fieldName = $field->name;
} else if($field instanceof Field) {
$fieldName = $field->name;
}
// results
$fieldCounts = array(); // counts indexed by field name (if count mode)
$items = $page->wire('pages')->newPageArray();
$itemsByField = array();
foreach($page->template->fieldgroup as $f) {
if($fieldName && $field->name != $fieldName) continue;
if(!$f->type instanceof FieldtypePage) continue;
if($byField) $itemsByField[$f->name] = $page->wire('pages')->newPageArray();
$value = $page->get($f->name);
if($value instanceof Page && $value->id) {
$items->add($value);
if($byField) $itemsByField[$f->name]->add($value);
$fieldCounts[$f->name] = 1;
} else if($value instanceof PageArray && $value->count()) {
$items->import($value);
if($byField) $itemsByField[$f->name]->import($value);
$fieldCounts[$f->name] = $value->count();
} else {
unset($itemsByField[$f->name]);
}
}
if($getCount) return $byField ? $fieldCounts : $items->count();
if($byField) return $itemsByField;
return $items;
}
/**
* Return number of pages this one is following (referencing) by way of Page references
*
* @param Page $page
* @param bool $field Optionally limit to field, or specify boolean true to return array of counts per field.
* @return int|array
*
*/
public function numReferencing(Page $page, $field = false) {
return $this->referencing($page, $field, true);
}
/**
* Find other pages linking to the given one by way contextual links is textarea/html fields
*
* @param Page $page
* @param string $selector
* @param bool|string|Field $field
* @param array $options
* - `getIDs` (bool): Return array of page IDs rather than Page instances. (default=false)
* - `getCount` (bool): Return a total count (int) of found pages rather than Page instances. (default=false)
* - `confirm` (bool): Confirm that the links are present by looking at the actual page field data. (default=true)
* You can specify false for this option to make it perform faster, but with a potentially less accurate result.
* @return PageArray|array|int
* @throws WireException
*
*/
public function links(Page $page, $selector = '', $field = false, array $options = array()) {
/** @var FieldtypeTextarea $fieldtype */
$fieldtype = $page->wire('fieldtypes')->get('FieldtypeTextarea');
if(!$fieldtype) throw new WireException('Unable to find FieldtypeTextarea');
return $fieldtype->findLinks($page, $selector, $field, $options);
}
/**
* Return total found number of pages linking to this one with no exclusions
*
* @param Page $page
* @param bool $field
* @return int
*
*/
public function numLinks(Page $page, $field = false) {
return $this->links($page, true, $field, array('getCount' => true));
}
/**
* Return total number of pages visible to current user linking to this one
*
* @param Page $page
* @param bool $field
* @return array|int|PageArray
*
*/
public function hasLinks(Page $page, $field = false) {
return $this->links($page, '', $field, array('getCount' => true));
}
/******************************************************************************************************************
* LEGACY METHODS
@@ -621,6 +988,7 @@ class PageTraversal {
$next = $page;
do {
/** @var Page $next */
$next = $siblings->getNext($next, false);
if(empty($selector) || !$next || $next->matches($selector)) break;
} while($next && $next->id);
@@ -664,6 +1032,7 @@ class PageTraversal {
$prev = $page;
do {
/** @var Page $prev */
$prev = $siblings->getPrev($prev, false);
if(empty($selector) || !$prev || $prev->matches($selector)) break;
} while($prev && $prev->id);
@@ -677,7 +1046,7 @@ class PageTraversal {
* @param Page $page
* @param string|array $selector Optional selector. When specified, will filter the found siblings.
* @param PageArray $siblings Optional siblings to use instead of the default.
* @return Page|NullPage Returns all matching pages after this one.
* @return PageArray Returns all matching pages after this one.
*
*/
public function nextAllSiblings(Page $page, $selector = '', PageArray $siblings = null) {
@@ -711,7 +1080,7 @@ class PageTraversal {
* @param Page $page
* @param string|array $selector Optional selector. When specified, will filter the found siblings.
* @param PageArray $siblings Optional siblings to use instead of the default.
* @return Page|NullPage Returns all matching pages before this one.
* @return PageArray
*
*/
public function prevAllSiblings(Page $page, $selector = '', PageArray $siblings = null) {
@@ -753,8 +1122,7 @@ class PageTraversal {
$siblings->prepend($page);
}
$siblings = $this->nextAll($page, '', $siblings);
$siblings = $this->nextAllSiblings($page, '', $siblings);
$all = $page->wire('pages')->newPageArray();
$stop = false;
@@ -805,8 +1173,7 @@ class PageTraversal {
$siblings->add($page);
}
$siblings = $this->prevAll($page, '', $siblings);
$siblings = $this->prevAllSiblings($page, '', $siblings);
$all = $page->wire('pages')->newPageArray();
$stop = false;

View File

@@ -12,11 +12,13 @@
* Pagefile objects are contained by a `Pagefiles` object.
* #pw-body
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
* @property-read string $url URL to the file on the server.
* @property-read string $URL Same as $url property but with browser cache busting query string appended that represents the file's modification time. #pw-group-other
* @property-read string $url URL to the file on the server.
* @property-read string $httpUrl URL to the file on the server including scheme and hostname.
* @property-read string $URL Same as $url property but with browser cache busting query string appended. #pw-group-other
* @property-read string $HTTPURL Same as the cache-busting uppercase “URL” property, but includes scheme and hostname. #pw-group-other
* @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.
@@ -26,10 +28,13 @@
* @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
* @property string $ext Files extension (i.e. last 3 or so characters)
* @property int $filesize File size (number of bytes).
* @property-read int $filesize File size (number of bytes).
* @property int $modified Unix timestamp of when Pagefile (file, description or tags) was last modified. #pw-group-date-time
* @property int $mtime Unix timestamp of when file (only) was last modified. #pw-group-date-time
* @property-read string $modifiedStr Readable date/time string of when Pagefile was last modified. #pw-group-date-time
* @property-read int $mtime Unix timestamp of when file (only) was last modified. #pw-group-date-time
* @property-read string $mtimeStr Readable date/time string when file (only) was last modified. #pw-group-date-time
* @property int $created Unix timestamp of when file was created. #pw-group-date-time
* @property-read string $createdStr Readable date/time string of when Pagefile was created #pw-group-date-time
* @property string $filesizeStr File size as a formatted string, i.e. “123 Kb”.
* @property Pagefiles $pagefiles The Pagefiles WireArray that contains this file. #pw-group-other
* @property Page $page The Page object that this file is part of. #pw-group-other
@@ -37,7 +42,8 @@
*
* @method void install($filename)
* @method string httpUrl()
*
* @method string noCacheURL($http = false)
*
*/
class Pagefile extends WireData {
@@ -54,7 +60,15 @@ class Pagefile extends WireData {
* @var Pagefiles
*
*/
protected $pagefiles;
protected $pagefiles;
/**
* Extra file data
*
* @var array
*
*/
protected $filedata = array();
/**
* Construct a new Pagefile
@@ -118,8 +132,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]");
@@ -164,14 +187,20 @@ class Pagefile extends 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)) {
@@ -186,16 +215,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;
@@ -207,27 +295,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
@@ -235,11 +342,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;
}
@@ -268,17 +379,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
@@ -291,6 +416,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
@@ -371,7 +503,10 @@ class Pagefile extends WireData {
break;
case 'URL':
// nocache url
$value = $this->url() . '?nc=' . @filemtime($this->filename());
$value = $this->noCacheURL();
break;
case 'HTTPURL':
$value = $this->noCacheURL(true);
break;
case 'pagefiles':
$value = $this->pagefiles;
@@ -390,14 +525,40 @@ class Pagefile extends WireData {
parent::set($key, $value);
}
break;
case 'modifiedStr':
case 'createdStr':
$value = parent::get(str_replace('Str', '', $key));
$value = wireDate($this->wire('config')->dateFormat, $value);
break;
case 'fileData':
case 'filedata':
$value = $this->filedata();
break;
case 'mtime':
case 'mtimeStr':
case 'filemtime':
case 'filemtimeStr':
$value = filemtime($this->filename());
if(strpos($key, 'Str')) $value = wireDate($this->wire('config')->dateFormat, $value);
break;
}
if(is_null($value)) return parent::get($key);
return $value;
}
/**
* Hookable no-cache URL
*
* #pw-internal
*
* @param bool $http Include scheme and hostname?
* @return string
*
*/
public function ___noCacheURL($http = false) {
return ($http ? $this->httpUrl() : $this->url()) . '?nc=' . @filemtime($this->filename());
}
/**
* Return the next sibling Pagefile in the parent Pagefiles, or NULL if at the end.
*
@@ -791,7 +952,7 @@ class Pagefile extends WireData {
*/
public function unlink() {
if(!strlen($this->basename) || !is_file($this->filename)) return true;
return unlink($this->filename);
return $this->wire('files')->unlink($this->filename, true);
}
/**
@@ -807,7 +968,7 @@ class Pagefile extends WireData {
*/
public function rename($basename) {
$basename = $this->pagefiles->cleanBasename($basename, true);
if(rename($this->filename, $this->pagefiles->path . $basename)) {
if($this->wire('files')->rename($this->filename, $this->pagefiles->path . $basename, true)) {
$this->set('basename', $basename);
return $this->basename();
}
@@ -820,7 +981,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) {
@@ -877,5 +1038,29 @@ class Pagefile extends WireData {
public function isTemp($set = null) {
return $this->pagefiles->isTemp($this, $set);
}
/**
* Debug info
*
* @return array
*
*/
public function __debugInfo() {
$filedata = $this->filedata();
if(empty($filedata)) $filedata = null;
$info = array(
'url' => $this->url(),
'filename' => $this->filename(),
'filesize' => $this->filesize(),
'description' => $this->description,
'tags' => $this->tags,
'created' => $this->createdStr,
'modified' => $this->modifiedStr,
'filemtime' => $this->mtimeStr,
'filedata' => $filedata,
);
if(empty($info['filedata'])) unset($info['filedata']);
return $info;
}
}

View File

@@ -38,7 +38,7 @@
* Typically a Pagefiles object will be associated with a specific field attached to a Page.
* There may be multiple instances of Pagefiles attached to a given Page (depending on what fields are in it's fieldgroup).
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
*
@@ -47,6 +47,7 @@
* @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
*
*/
@@ -75,6 +76,22 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
*
*/
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
@@ -320,6 +337,18 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
if(is_string($item)) {
/** @var Pagefile $item */
$item = $this->wire(new Pagefile($this, $item));
} else if($item instanceof Pagefile) {
$page = $this->get('page');
if($page && "$page" !== "$item->page") {
$newItem = clone $item;
$newItem->setPagefilesParent($this);
$newItem->install($item->filename);
$newItem->isTemp(true);
$this->unTempQueue($newItem);
$this->message("Copied $item->url to $newItem->url", Notice::debug);
$item = $newItem;
}
}
/** @var Pagefiles $result */
@@ -334,14 +363,36 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
*
*/
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
@@ -375,9 +426,7 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
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;
@@ -401,6 +450,87 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
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];
if(!$n) $n = 1;
}
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
*
@@ -521,6 +651,59 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
}
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
@@ -665,6 +848,19 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
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?
*
@@ -721,5 +917,29 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
return $this->formatted;
}
/**
* Debug info
*
* @return array
*
*/
public function __debugInfo() {
$info = array(
'count' => $this->count(),
'page' => $this->page ? $this->page->path() : '?',
'field' => $this->field ? $this->field->name : '?',
'url' => $this->url(),
'path' => $this->path(),
'items' => array(),
);
foreach($this as $key => $pagefile) {
/** @var Pagefile $pagefile */
$info['items'][$key] = $pagefile->__debugInfo();
}
return $info;
}
}

View File

@@ -230,6 +230,40 @@ class PagefilesManager extends Wire {
return $this->_copyFiles($this->path(), $toPath);
}
/**
* Copy/import files from given path into the pages files directory
*
* #pw-group-manipulation
*
* @param string $fromPath Path to copy/import files from.
* @param bool $move Move files into directory rather than copy?
* @return int Number of files/directories copied.
* @since 3.0.114
*
*/
public function importFiles($fromPath, $move = false) {
return $this->_copyFiles($fromPath, $this->path(), $move);
}
/**
* Replace all pages files with those from given path
*
* #pw-group-manipulation
*
* @param string $fromPath
* @param bool $move Move files to destination rather than copy? (default=false)
* @return int Number of files/directories copied.
* @throws WireException if given a path that does not exist.
* @since 3.0.114
*
*
*/
public function replaceFiles($fromPath, $move = false) {
if(!is_dir($fromPath)) throw new WireException("Path does not exist: $fromPath");
$this->emptyPath();
return $this->_copyFiles($fromPath, $this->path(), $move);
}
/**
* Recursively move all files managed by this PagefilesManager into a new path.
*
@@ -284,16 +318,16 @@ class PagefilesManager extends Wire {
$errors = 0;
if($recursive) {
// clear out path and everything below it
if(!wireRmdir($path, true)) $errors++;
if(!$this->wire('files')->rmdir($path, true, true)) $errors++;
if(!$rmdir) $this->_createPath($path);
} else {
// only clear out files in path
foreach(new \DirectoryIterator($path) as $file) {
if($file->isDot() || $file->isDir()) continue;
if(!unlink($file->getPathname())) $errors++;
if(!$this->wire('files')->unlink($file->getPathname(), true)) $errors++;
}
if($rmdir) {
@rmdir($path); // will not be successful if other dirs within it
$this->wire('files')->rmdir($path, false, true); // will not be successful if other dirs within it
}
}
return $errors === 0;
@@ -572,15 +606,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

@@ -24,20 +24,25 @@
* ~~~~~
* #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 string $focusStr Readable string containing focus information.
* @property-read bool $hasFocus Does this image have custom focus settings? (i.e. $focus['default'] == true)
* @property-read array $suffix Array containing file suffix(es).
* @property-read string $suffixStr String of file suffix(es) separated by comma.
*
* @method bool|array isVariation($basename, $allowSelf = false)
* @method bool|array isVariation($basename, $options = array())
* @method Pageimage crop($x, $y, $width, $height, $options = array())
* @method array rebuildVariations($mode = 0, array $suffix = array(), array $options = array())
* @method install($filename)
@@ -177,6 +182,133 @@ 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" or "top left zoom", i.e. "25 70 30" (percentages).
* - SET: Specify CSV key=value string in the format "top=25%, left=70%, zoom=30%" in any order
* - 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) && $left === null) {
if(strpos($top, '=')) {
// SET string "top=25%, left=70%, zoom=0%"
$a = array('top' => 50, 'left' => 50, 'zoom' => 0);
$parts = explode(',', str_replace(array(' ', '%'), '', $top));
foreach($parts as $part) {
if(!strpos($part, '=')) continue;
list($name, $pct) = explode('=', $part);
$a[$name] = strpos($pct, '.') !== false ? (float) $pct : (int) $pct;
}
$top = $a; // for later setting by array
unset($a);
} else if(strpos($top, ' ')) {
// 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
*
@@ -206,6 +338,22 @@ class Pageimage extends Pagefile {
case 'error':
$value = $this->error;
break;
case 'focus':
$value = $this->focus();
break;
case 'focusStr':
$focus = $this->focus();
$value = "top=$focus[top]%,left=$focus[left]%,zoom=$focus[zoom]%" . ($focus['default'] ? " (default)" : "");
break;
case 'hasFocus':
$value = $this->focus(true);
break;
case 'suffix':
$value = $this->suffix();
break;
case 'suffixStr':
$value = implode(',', $this->suffix());
break;
default:
$value = parent::get($key);
}
@@ -217,7 +365,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
*
*/
@@ -226,25 +375,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
);
}
/**
@@ -291,7 +488,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).
@@ -301,10 +498,17 @@ 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.
* - `allowOriginal` (bool): Return original if already at width/height? May not be combined with other options. (default=false)
*
* **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".
@@ -315,7 +519,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.
@@ -362,16 +568,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)) {
@@ -398,6 +594,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
@@ -406,6 +604,11 @@ 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
'allowOriginal' => false, // Return original image if already at requested dimensions? (must be only specified option)
);
$this->error = '';
@@ -413,16 +616,32 @@ 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['allowOriginal'] && count($requestOptions) === 1) {
if((!$width || $this->width() == $width) && (!$height || $this->height() == $height)) {
// return original image if already at requested width/height
return $this;
}
}
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']);
}
@@ -432,9 +651,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'])) {
@@ -453,23 +678,31 @@ 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);
if($exists && $options['forceNew']) $this->wire('files')->unlink($filenameFinal, true);
if(file_exists($filenameUnvalidated)) $this->wire('files')->unlink($filenameUnvalidated, true);
if(@copy($this->filename(), $filenameUnvalidated)) {
try {
@@ -527,8 +760,8 @@ class Pageimage extends Pagefile {
// if an error occurred, that error property will be populated with details
if($this->error) {
// error condition: unlink copied file
if(is_file($filenameFinal)) @unlink($filenameFinal);
if($filenameUnvalidated && is_file($filenameUnvalidated)) @unlink($filenameUnvalidated);
if(is_file($filenameFinal)) $this->wire('files')->unlink($filenameFinal, true);
if($filenameUnvalidated && is_file($filenameUnvalidated)) $this->wire('files')->unlink($filenameUnvalidated);
// write an invalid image so it's clear something failed
// todo: maybe return a 1-pixel blank image instead?
@@ -787,18 +1020,43 @@ class Pageimage extends Pagefile {
*
* @param int $width Max allowed width
* @param int $height Max allowed height
* @param array $options See `Pageimage::size()` method for options
* @param array $options See `Pageimage::size()` method for options, or these additional options:
* - `allowOriginal` (bool): Allow original image to be returned if already within max requested dimensions? (default=false)
* @return Pageimage
*
*/
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);
$defaults = array(
'allowOriginal' => false,
'upscaling' => false,
'cropping' => false
);
$options = array_merge($defaults, $options);
$adjustedWidth = $width < 1 || $this->width() <= $width ? 0 : $width;
$adjustedHeight = $height < 1 || $this->height() <= $height ? 0 : $height;
// if already within maxSize dimensions then do nothing
if(!$adjustedWidth && !$adjustedHeight) {
if($options['allowOriginal']) return $this; // image already within target
$adjustedWidth = $width;
$options['nameHeight'] = $height;
} else if(!$adjustedWidth) {
$options['nameWidth'] = $width;
} else if(!$adjustedHeight) {
$options['nameHeight'] = $height;
}
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($adjustedWidth, $adjustedHeight, $options);
}
/**
@@ -810,7 +1068,10 @@ class Pageimage extends Pagefile {
* #pw-group-variations
*
* @param array $options Optional, one or more options in an associative array of the following:
* - `info` (bool): when true, method returns variation info arrays rather than Pageimage objects
* - `info` (bool): when true, method returns variation info arrays rather than Pageimage objects (default=false).
* - `verbose` (bool): Return verbose array of info. If false, returns only filenames (default=true).
* This option does nothing unless the `info` option is true. Also note that if verbose is false, then all options
* following this one no longer apply (since it is no longer returning width/height info).
* - `width` (int): only variations with given width will be returned
* - `height` (int): only variations with given height will be returned
* - `width>=` (int): only variations with width greater than or equal to given will be returned
@@ -818,24 +1079,56 @@ class Pageimage extends Pagefile {
* - `width<=` (int): only variations with width less than or equal to given will be returned
* - `height<=` (int): only variations with height less than or equal to given will be returned
* - `suffix` (string): only variations having the given suffix will be returned
* - `suffixes` (array): only variations having one of the given suffixes will be returned
* - `noSuffix` (string): exclude variations having this suffix
* - `noSuffixes` (array): exclude variations having any of these suffixes
* - `name` (string): only variations containing this text in filename will be returned (case insensitive)
* - `noName` (string): only variations NOT containing this text in filename will be returned (case insensitive)
* - `regexName` (string): only variations that match this PCRE regex will be returned
* @return Pageimages|array Returns Pageimages array of Pageimage instances.
* Only returns regular array if provided `$options['info']` is true.
*
*/
public function getVariations(array $options = array()) {
if(!is_null($this->variations)) return $this->variations;
$variations = $this->wire(new Pageimages($this->pagefiles->page));
if(!is_null($this->variations) && empty($options)) return $this->variations;
$defaults = array(
'info' => false,
'verbose' => true,
);
$options = array_merge($defaults, $options);
if(!$options['verbose'] && !$options['info']) $options['verbose'] = true; // non-verbose only allowed if info==true
$variations = $options['info'] ? null : $this->wire(new Pageimages($this->pagefiles->page));
$dir = new \DirectoryIterator($this->pagefiles->path);
$infos = array();
// if suffix or noSuffix option contains space, convert it to suffixes or noSuffixes array option
foreach(array('suffix', 'noSuffix') as $key) {
if(!isset($options[$key])) continue;
if(strpos(trim($options['suffix']), ' ') === false) continue;
$keyPlural = $key . 'es';
$value = isset($options[$keyPlural]) ? $options[$keyPlural] : array();
$options[$keyPlural] = array_merge($value, explode(' ', trim($options[$key])));
unset($options[$key]);
}
foreach($dir as $file) {
if($file->isDir() || $file->isDot()) continue;
$info = $this->isVariation($file->getFilename());
$info = $this->isVariation($file->getFilename(), array('verbose' => $options['verbose']));
if(!$info) continue;
if($options['info'] && !$options['verbose']) {
$infos[] = $info;
continue;
}
$allow = true;
if(count($options)) foreach($options as $option => $value) {
foreach($options as $option => $value) {
switch($option) {
case 'width': $allow = $info['width'] == $value; break;
case 'width>=': $allow = $info['width'] >= $value; break;
@@ -843,10 +1136,34 @@ class Pageimage extends Pagefile {
case 'height': $allow = $info['height'] == $value; break;
case 'height>=': $allow = $info['height'] >= $value; break;
case 'height<=': $allow = $info['height'] <= $value; break;
case 'name': $allow = stripos($file->getBasename(), $value) !== false; break;
case 'noName': $allow = stripos($file->getBasename(), $value) === false; break;
case 'regexName': $allow = preg_match($value, $file->getBasename()); break;
case 'suffix': $allow = in_array($value, $info['suffix']); break;
case 'noSuffix': $allow = !in_array($value, $info['suffix']); break;
case 'suffixes':
// any one of given suffixes will allow the variation
$allow = false;
foreach($value as $suffix) {
$allow = in_array($suffix, $info['suffix']);
if($allow) break;
}
break;
case 'noSuffixes':
// any one of the given suffixes will disallow the variation
$allow = true;
foreach($value as $noSuffix) {
if(!in_array($noSuffix, $info['suffix'])) continue;
$allow = false;
break;
}
break;
}
if(!$allow) break;
}
if(!$allow) continue;
if(!empty($options['info'])) {
$infos[$file->getBasename()] = $info;
} else {
@@ -859,12 +1176,11 @@ class Pageimage extends Pagefile {
}
}
if(!empty($options['info'])) {
return $infos;
} else {
$this->variations = $variations;
return $variations;
}
if(!empty($options['info'])) return $infos;
if(empty($options)) $this->variations = $variations;
return $variations;
}
/**
@@ -878,6 +1194,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
@@ -905,7 +1222,7 @@ class Pageimage extends Pagefile {
$options['forceNew'] = true;
foreach($this->getVariations(array('info' => true)) as $info) {
$o = $options;
unset($o['cropping']);
$skip = false;
@@ -949,6 +1266,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;
@@ -957,14 +1279,36 @@ class Pageimage extends Pagefile {
// rebuild the variation
$o['forceNew'] = true;
$o['suffix'] = $info['suffix'];
if(is_file($info['path'])) unlink($info['path']);
if(is_file($info['path'])) $this->wire('files')->unlink($info['path'], true);
/*
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);
}
@@ -992,8 +1336,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
*
@@ -1006,15 +1352,26 @@ class Pageimage extends Pagefile {
* #pw-group-variations
*
* @param string $basename Filename to check (basename, which excludes path)
* @param bool $allowSelf When true, it will return variation info even if same as current Pageimage.
* @return bool|array Returns false if not a variation, or array of info if it is.
* @param array|bool $options Array of options to modify behavior, or boolean to only specify `allowSelf` option.
* - `allowSelf` (bool): When true, it will return variation info even if same as current Pageimage. (default=false)
* - `verbose` (bool): Return verbose array of info? If false, just returns basename (string) or false. (default=true)
* @return bool|string|array Returns false if not a variation, or array (verbose) or string (non-verbose) of info if it is.
*
*/
public function ___isVariation($basename, $allowSelf = false) {
public function ___isVariation($basename, $options = array()) {
$defaults = array(
'allowSelf' => false,
'verbose' => true,
);
if(!is_array($options)) $options = array('allowSelf' => (bool) $options);
$options = array_merge($defaults, $options);
static $level = 0;
$variationName = basename($basename);
$originalName = $this->basename;
$info = array();
// that that everything from the beginning up to the first period is exactly the same
// otherwise, they are different source files
@@ -1032,7 +1389,7 @@ class Pageimage extends Pagefile {
}
// if file is the same as the original, then it's not a variation
if(!$allowSelf && $variationName == $this->basename) return false;
if(!$options['allowSelf'] && $variationName == $this->basename) return false;
// if file doesn't start with the original name then it's not a variation
if(strpos($variationName, $originalName) !== 0) return false;
@@ -1055,16 +1412,14 @@ class Pageimage extends Pagefile {
// identify parent and any parent suffixes
$suffixAll = array();
while(($pos = strrpos($base, '.')) !== false) {
$part = substr($base, $pos+1);
// if(is_null($parent)) {
// $parent = substr($base, 0, $pos) . $ext;
//$parent = $originalName . "." . $part . $ext;
// }
$base = substr($base, 0, $pos);
while(($rpos = strrpos($part, '-')) !== false) {
$suffixAll[] = substr($part, $rpos+1);
$part = substr($part, 0, $rpos);
if($options['verbose']) {
while(($pos = strrpos($base, '.')) !== false) {
$part = substr($base, $pos + 1);
$base = substr($base, 0, $pos);
while(($rpos = strrpos($part, '-')) !== false) {
$suffixAll[] = substr($part, $rpos + 1);
$part = substr($part, 0, $rpos);
}
}
}
@@ -1089,7 +1444,7 @@ class Pageimage extends Pagefile {
// if regex does not match, return false
if(preg_match($re1, $meat, $matches)) {
// this is a variation with dimensions, return array of info
$info = array(
if($options['verbose']) $info = array(
'name' => $basename,
'url' => $this->pagefiles->url . $basename,
'path' => $this->pagefiles->path . $basename,
@@ -1103,7 +1458,7 @@ class Pageimage extends Pagefile {
} else if(preg_match($re2, $meat, $matches)) {
// this is a variation only with suffix
$info = array(
if($options['verbose']) $info = array(
'name' => $basename,
'url' => $this->pagefiles->url . $basename,
'path' => $this->pagefiles->path . $basename,
@@ -1117,9 +1472,15 @@ class Pageimage extends Pagefile {
} else {
return false;
}
$info['hidpiWidth'] = $this->hidpiWidth(0, $info['width']);
$info['hidpiHeight'] = $this->hidpiWidth(0, $info['height']);
// if not in verbose mode, just return variation basename
if(!$options['verbose']) return $variationName;
$actualInfo = $this->getImageInfo($info['path']);
$info['actualWidth'] = $actualInfo['width'];
$info['actualHeight'] = $actualInfo['height'];
$info['hidpiWidth'] = $this->hidpiWidth(0, $info['actualWidth']);
$info['hidpiHeight'] = $this->hidpiWidth(0, $info['actualHeight']);
if(empty($info['crop'])) {
// attempt to extract crop info from suffix
@@ -1152,19 +1513,41 @@ class Pageimage extends Pagefile {
*
* #pw-group-variations
*
* @return $this
* @param array $options See options for getVariations() method to limit what variations are removed, plus these:
* - `dryRun` (bool): Do not remove now and instead only return the filenames of variations that would be deleted (default=false).
* - `getFiles` (bool): Return deleted filenames? Also assumed if the test option is used (default=false).
* @return $this|array Returns $this by default, or array of deleted filenames if the `returnFiles` option is specified
*
*/
public function removeVariations() {
public function removeVariations(array $options = array()) {
$defaults = array(
'dryRun' => false,
'getFiles' => false
);
$variations = $this->getVariations();
$variations = $this->getVariations($options);
if(!empty($options['dryrun'])) $defaults['dryRun'] = $options['dryrun']; // case insurance
$options = array_merge($defaults, $options); // placement after getVariations() intended
$deletedFiles = array();
/** @var WireFileTools $files */
$files = $this->wire('files');
foreach($variations as $variation) {
if(is_file($variation->filename)) unlink($variation->filename);
$filename = $variation->filename;
if(!is_file($filename)) continue;
if($options['dryRun']) {
$success = true;
} else {
$success = $files->unlink($filename, true);
}
if($success) $deletedFiles[] = $filename;
}
$this->variations = null;
return $this;
if(!$options['dryRun']) $this->variations = null;
return ($options['dryRun'] || $options['getFiles'] ? $deletedFiles : $this);
}
/**
@@ -1250,5 +1633,35 @@ class Pageimage extends Pagefile {
}
}
/**
* Debug info
*
* @return array
*
*/
public function __debugInfo() {
static $depth = 0;
$depth++;
$info = parent::__debugInfo();
$info['width'] = $this->width();
$info['height'] = $this->height();
$info['suffix'] = $this->suffixStr;
if($this->hasFocus) $info['focus'] = $this->focusStr;
if(isset($info['filedata']) && isset($info['filedata']['focus'])) unset($info['filedata']['focus']);
if(empty($info['filedata'])) unset($info['filedata']);
$original = $this->original;
if($original && $original !== $this) $info['original'] = $original->basename;
if($depth < 2) {
$info['variations'] = array();
$variations = $this->getVariations(array('info' => true, 'verbose' => false));
foreach($variations as $name) {
$info['variations'][] = $name;
}
if(empty($info['variations'])) unset($info['variations']);
}
$depth--;
return $info;
}
}

View File

@@ -29,13 +29,13 @@
* @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 int|array emptyTrash(array $options = array()) Empty the trash and return number of pages deleted. #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 string setupPageName(Page $page, array $options = array()) Determine and populate a name for the given page. #pw-internal
* @method void insertBefore(Page $page, Page $beforePage) Insert one page as a sibling before another. #pw-advanced
* @method void insertAfter(Page $page, Page $afterPage) Insert one page as a sibling after another. #pw-advanced
*
@@ -122,6 +122,12 @@ class Pages extends Wire {
*/
protected $editor;
/**
* @var PagesNames
*
*/
protected $names;
/**
* @var PagesLoaderCache
*
@@ -394,13 +400,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()
@@ -612,13 +619,14 @@ class Pages extends Wire {
*
* #pw-group-manipulation
*
* @return int Returns total number of pages deleted from trash.
* @param array $options See PagesTrash::emptyTrash() for advanced options
* @return int|array Returns total number of pages deleted from trash, or array if verbose option specified.
* This number is negative or 0 if not all pages could be deleted and error notices may be present.
* @see Pages::trash(), Pages::restore()
*
*/
public function ___emptyTrash() {
return $this->trasher()->emptyTrash();
public function ___emptyTrash(array $options = array()) {
return $this->trasher()->emptyTrash($options);
}
/**
@@ -644,6 +652,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:**
@@ -1195,11 +1204,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);
@@ -1216,6 +1231,7 @@ class Pages extends Wire {
* @param array $options Optionally specify array of any of the following:
* - `pageClass` (string): Class to use for Page object (default='Page').
* - `template` (Template|id|string): Template to use.
* - Plus any other Page properties or fields you want to set at this time
* @return Page
*
*/
@@ -1234,9 +1250,16 @@ class Pages extends Wire {
} else {
$template = null;
}
$class = wireClassName($class, true);
$page = $this->wire(new $class($template));
if(!$page instanceof Page) $page = $this->wire(new Page($template));
unset($options['pageClass'], $options['template']);
foreach($options as $name => $value) {
$page->set($name, $value);
}
return $page;
}
@@ -1327,6 +1350,17 @@ class Pages extends Wire {
if(!$this->editor) $this->editor = $this->wire(new PagesEditor($this));
return $this->editor;
}
/**
* @return PagesNames
*
* #pw-internal
*
*/
public function names() {
if(!$this->names) $this->names = $this->wire(new PagesNames($this));
return $this->names;
}
/**
* @return PagesLoaderCache

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,27 +15,19 @@ 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
*
* @var string
*
*/
protected $untitledPageName = 'untitled';
/**
* @var Pages
*
*/
protected $pages;
protected $pages;
public function __construct(Pages $pages) {
$this->pages = $pages;
@@ -44,9 +36,16 @@ class PagesEditor extends Wire {
$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;
}
/**
@@ -129,9 +128,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 */
@@ -148,14 +146,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) {
@@ -180,42 +187,87 @@ 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;
$isSystem = $page->hasStatus(Page::statusSystem) || $page->hasStatus(Page::statusSystemID);
$toTrash = $newParent->id > 0 && $newParent->isTrash();
$wasTrash = $oldParent->id > 0 && $oldParent->isTrash();
// page was moved
if($page->template->noMove && ($isSystem || (!$toTrash && !$wasTrash))) {
// make sure the page template allows moves.
// only move always allowed is to the trash (or out of it), unless page has system status
$reason =
sprintf($this->_('Page using template “%s” is not moveable.'), $page->template->name) . ' ' .
"(Template::noMove) [{$oldParent->path} => {$newParent->path}]";
} else if($newParent->template->noChildren) {
// check if new parent disallows children
$reason = sprintf(
$this->_('Chosen parent “%1$s” uses template “%2$s” that does not allow children.'),
$newParent->path,
$newParent->template->name
);
} 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 = sprintf(
$this->_('Cannot move “%1$s” because template “%2$s” used by page “%3$s” does not allow children using template “%4$s”.'),
$page->name,
$newParent->template->name,
$newParent->path,
$page->template->name
);
} else if(count($page->template->parentTemplates) && $newParent->id != $config->trashPageID
&& !in_array($newParent->template->id, $page->template->parentTemplates)) {
// check for allowed parentTemplates setting
$reason = sprintf(
$this->_('Cannot move “%1$s” because template “%2$s” used by new parent “%3$s” is not allowed by moved page template “%4$s”.'),
$page->name,
$newParent->template->name,
$newParent->path,
$page->template->name
);
} else if(count($newParent->children("name=$page->name, id!=$page->id, include=all"))) {
// check for page name collision
$reason = sprintf(
$this->_('Chosen parent “%1$s” already has a page named “%2$s”.'),
$newParent->path,
$page->name
);
} else {
$moveable = true;
}
return $moveable;
}
/**
* Is the given page deleteable from the API?
@@ -239,18 +291,22 @@ class PagesEditor extends Wire {
* Auto-populate some fields for a new page that does not yet exist
*
* Currently it does this:
*
* - Assigns a parent if one is not already assigned.
* - Sets up a unique page->name based on the format or title if one isn't provided already.
* - Assigns a 'sort' value'.
* - Assigns a sort value.
* - Populates any default values for fields.
*
* @param Page $page
* @throws \Exception|WireException|\PDOException if failure occurs while in DB transaction
*
*/
public function setupNew(Page $page) {
$parent = $page->parent();
if(!$parent->id) {
// auto-assign a parent, if we can find one in family settings
// assign parent
if(!$parent->id) {
$parentTemplates = $page->template->parentTemplates;
$parent = null;
@@ -263,13 +319,17 @@ class PagesEditor extends Wire {
if($parent->id) $page->parent = $parent;
}
if(!strlen($page->name)) $this->pages->setupPageName($page);
// assign page name
if(!strlen($page->name)) {
$this->pages->setupPageName($page); // call through $pages intended, so it can be hooked
}
// assign sort order
if($page->sort < 0) {
// auto assign a sort
$page->sort = $page->parent->numChildren();
}
// assign any default values for fields
foreach($page->template->fieldgroup as $field) {
if($page->isLoaded($field->name)) continue; // value already set
if(!$page->hasField($field)) continue; // field not valid for page
@@ -284,6 +344,7 @@ class PagesEditor extends Wire {
}
} catch(\Exception $e) {
$this->trackException($e, false, true);
if($this->wire('database')->inTransaction()) throw $e;
}
}
}
@@ -304,115 +365,7 @@ class PagesEditor extends Wire {
*
*/
public function setupPageName(Page $page, array $options = array()) {
$defaults = array(
'format' => '',
);
$options = array_merge($defaults, $options);
$format = $options['format'];
$sanitizer = $this->wire('sanitizer');
if(strlen($page->name)) {
// make sure page starts with "untitled" or "untitled-"
if($page->name != $this->untitledPageName && strpos($page->name, "$this->untitledPageName-") !== 0) {
// page already has a name and it's not a temporary/untitled one
// so we do nothing
return '';
}
// page starts with our untitled name, but is it in the exact format we use?
if($page->name != $this->untitledPageName) {
$parts = explode('-', $page->name);
array_shift($parts); // shift off 'untitled';
$parts = implode('', $parts); // put remaining back together
// if we were left with something other than digits,
// this is not an auto-generated name, so leave as-is
if(!ctype_digit($parts)) return '';
}
}
if(!strlen($format)) {
$parent = $page->parent();
if($parent && $parent->id) $format = $parent->template->childNameFormat;
}
if(!strlen($format)) {
if(strlen($page->title)) {
// default format is title
$format = 'title';
} else {
// if page has no title, default format is date
$format = 'Y-m-d H:i:s';
}
}
$pageName = '';
if(strlen($format)) {
// @todo add option to auto-gen name from any page property/field
if($format == 'title') {
if(strlen($page->title)) $pageName = $page->title;
else $pageName = $this->untitledPageName;
} else if(!ctype_alnum($format) && !preg_match('/^[-_a-zA-Z0-9]+$/', $format)) {
// it is a date format
$pageName = date($format);
} else {
// predefined format
$pageName = $format;
}
} else if(strlen($page->title)) {
$pageName = $page->title;
} else {
// no name will be assigned
}
if($pageName == $this->untitledPageName && strpos($page->name, $this->untitledPageName) === 0) {
// page already has untitled name, and there's no need to re-assign the untitled name
return '';
}
$name = '';
if(strlen($pageName)) {
// make the name unique
if($this->wire('config')->pageNameCharset === 'UTF8') {
$pageName = $sanitizer->pageNameUTF8($pageName);
} else {
$pageName = $sanitizer->pageName($pageName, Sanitizer::translate);
}
$numChildren = $page->parent->numChildren();
$n = 0;
do {
$name = $pageName;
if($n > 0) {
$nStr = "-" . ($numChildren + $n);
if($n > 100) {
// if we've reached this many dups, start adding a random element to it
$nStr = '_' . mt_rand() . $nStr;
}
if(strlen($name) + strlen($nStr) > Pages::nameMaxLength) {
$name = substr($name, 0, Pages::nameMaxLength - strlen($nStr));
}
$name .= $nStr;
}
$n++;
} while($n < 200 && $this->pages->count("parent=$page->parent, name=" . $sanitizer->selectorValue($name) . ", include=all"));
if($this->pages->count("parent=$page->parent, name=" . $sanitizer->selectorValue($name) . ", include=all") > 0) {
// this is now extremely unlikely
throw new WireException("Unable to generate unique name for page $page->id");
}
$page->name = $sanitizer->pageNameUTF8($name);
$page->set('_hasAutogenName', true); // for savePageQuery, provides adjustName behavior for new pages
}
return $name;
return $this->pages->names()->setupNewPageName($page, isset($options['format']) ? $options['format'] : '');
}
/**
@@ -433,6 +386,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
*
@@ -446,6 +400,7 @@ class PagesEditor extends Wire {
'forceID' => 0,
'ignoreFamily' => false,
'noHooks' => false,
'noFields' => false,
);
if(is_string($options)) $options = Selectors::keyValueStringToArray($options);
@@ -466,7 +421,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) {
@@ -517,7 +472,9 @@ class PagesEditor extends Wire {
}
$sql = '';
if(strpos($page->name, $this->untitledPageName) === 0) $this->pages->setupPageName($page);
if($this->pages->names()->isUntitledPageName($page->name)) {
$this->pages->setupPageName($page);
}
$data = array(
'parent_id' => (int) $page->parent_id,
@@ -555,7 +512,9 @@ class PagesEditor extends Wire {
} else if($isNew) {
$sql = 'modified=NOW()';
}
if(!$isNew && $page->created > 0) $data['created'] = date('Y-m-d H:i:s', $page->created);
if($page->created > 0) {
$data['created'] = date('Y-m-d H:i:s', $page->created);
}
}
if(isset($data['modified_users_id'])) $page->modified_users_id = $data['modified_users_id'];
@@ -574,8 +533,9 @@ class PagesEditor extends Wire {
$sql = trim($sql, ", ");
if($isNew) {
$query = $database->prepare("INSERT INTO pages SET $sql, created=NOW()");
if($isNew) {
if(empty($data['created'])) $sql .= ', created=NOW()';
$query = $database->prepare("INSERT INTO pages SET $sql");
} else {
$query = $database->prepare("UPDATE pages SET $sql WHERE id=:page_id");
$query->bindValue(":page_id", (int) $page->id, \PDO::PARAM_INT);
@@ -586,53 +546,19 @@ class PagesEditor extends Wire {
$query->bindValue(":$column", $value, is_int($value) ? \PDO::PARAM_INT : \PDO::PARAM_STR);
}
$n = 0;
$tries = 0;
$maxTries = 100;
do {
$result = false;
$errorCode = 0;
$keepTrying = false;
try {
$result = false;
$result = $database->execute($query);
} catch(\Exception $e) {
$errorCode = $e->getCode();
// while setupNew() already attempts to uniqify a page name with an incrementing
// number, there is a chance that two processes running at once might end up with
// the same number, so we account for the possibility here by re-trying queries
// that trigger duplicate-entry exceptions
if($errorCode == 23000 && ($page->_hasAutogenName || $options['adjustName'])) {
// Integrity constraint violation: 1062 Duplicate entry 'background-3552' for key 'name3894_parent_id'
// attempt to re-generate page name
$nameField = 'name';
// account for the duplicate possibly being a multi-language name field
if($this->wire('languages') && preg_match('/\b(name\d*)_parent_id\b/', $e->getMessage(), $matches)) $nameField = $matches[1];
// get either 'name' or 'name123' (where 123 is language ID)
$pageName = $page->$nameField;
// determine if current name format already has a trailing number
if(preg_match('/^(.+?)-(\d+)$/', $pageName, $matches)) {
// page already has a trailing number
$n = (int) $matches[2];
$pageName = $matches[1];
}
$nStr = '-' . (++$n);
if(strlen($pageName) + strlen($nStr) > Pages::nameMaxLength) $pageName = substr($pageName, 0, Pages::nameMaxLength - strlen($nStr));
$page->name = $pageName . $nStr;
$query->bindValue(":$nameField", $this->wire('sanitizer')->pageName($page->name, Sanitizer::toAscii));
} else {
// a different exception that we don't catch, so re-throw it
throw $e;
}
$keepTrying = $this->savePageQueryException($page, $query, $e, $options);
if(!$keepTrying) throw $e;
}
} while($errorCode == 23000 && (++$tries < $maxTries));
} while($keepTrying && (++$tries < $maxTries));
if($result && ($isNew || !$page->id)) $page->id = $database->lastInsertId();
if($options['forceID']) $page->id = (int) $options['forceID'];
@@ -640,6 +566,50 @@ class PagesEditor extends Wire {
return $result;
}
/**
* Handle Exception for savePageQuery()
*
* While setupNew() already attempts to uniqify a page name with an incrementing
* number, there is a chance that two processes running at once might end up with
* the same number, so we account for the possibility here by re-trying queries
* that trigger duplicate-entry exceptions.
*
* Example of actual exception text, for reference:
* Integrity constraint violation: 1062 Duplicate entry 'background-3552' for key 'name3894_parent_id'
*
* @param Page $page
* @param \PDOStatement $query
* @param \PDOException|\Exception $exception
* @param array $options
* @return bool True if it should give $query another shot, false if not
*
*/
protected function savePageQueryException(Page $page, $query, $exception, array $options) {
$errorCode = $exception->getCode();
// 23000=integrity constraint violation, duplicate entry
if($errorCode != 23000) return false;
if(!$this->pages->names()->hasAutogenName($page) && !$options['adjustName']) return false;
// account for the duplicate possibly being a multi-language name field
// i.e. “Duplicate entry 'bienvenido-2-1001' for key 'name1013_parent_id'”
if($this->wire('languages') && preg_match('/\b(name\d*)_parent_id\b/', $exception->getMessage(), $matches)) {
$nameField = $matches[1];
} else {
$nameField = 'name';
}
// get either 'name' or 'name123' (where 123 is language ID)
$pageName = $page->get($nameField);
$pageName = $this->pages->names()->incrementName($pageName);
$page->set($nameField, $pageName);
$query->bindValue(":$nameField", $this->wire('sanitizer')->pageName($pageName, Sanitizer::toAscii));
return true;
}
/**
* Save individual Page fields and supporting actions
*
@@ -649,11 +619,12 @@ class PagesEditor extends Wire {
* @param bool $isNew
* @param array $options
* @return bool
* @throws \Exception|WireException|\PDOException If any field-saving failure occurs while in a DB transaction
*
*/
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
@@ -688,14 +659,18 @@ 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);
if($this->wire('database')->inTransaction()) throw $e;
}
}
}
@@ -703,7 +678,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) {
@@ -918,51 +903,87 @@ class PagesEditor extends Wire {
* While this can be performed with other methods, this is here just to make it fast for internal/non-api use.
* See the trash and restore methods for an example.
*
* @param int $pageID
* @param int|array|Page|PageArray $pageID Page ID, Page, array of page IDs, or PageArray
* @param int $status Status per flags in Page::status* constants. Status will be OR'd with existing status, unless $remove option is set.
* @param bool $recursive Should the status descend into the page's children, and grandchildren, etc?
* @param bool $remove Should the status be removed rather than added?
* @return int Number of pages updated
*
*/
public function savePageStatus($pageID, $status, $recursive = false, $remove = false) {
$pageID = (int) $pageID;
$status = (int) $status;
$sql = $remove ? "status & ~$status" : $sql = "status|$status";
$sql = $remove ? "status & ~$status" : "status|$status";
$database = $this->wire('database');
$query = $database->prepare("UPDATE pages SET status=$sql WHERE id=:page_id");
$query->bindValue(":page_id", $pageID, \PDO::PARAM_INT);
$database->execute($query);
if($recursive) {
$parentIDs = array($pageID);
do {
$parentID = array_shift($parentIDs);
// update all children to have the same status
$query = $database->prepare("UPDATE pages SET status=$sql WHERE parent_id=:parent_id");
$query->bindValue(":parent_id", $parentID, \PDO::PARAM_INT);
$database->execute($query);
// locate children that themselves have children
$query = $database->prepare(
"SELECT pages.id FROM pages " .
"JOIN pages AS pages2 ON pages2.parent_id=pages.id " .
"WHERE pages.parent_id=:parent_id " .
"GROUP BY pages.id " .
"ORDER BY pages.sort"
);
$query->bindValue(':parent_id', $parentID, \PDO::PARAM_INT);
$database->execute($query);
/** @noinspection PhpAssignmentInConditionInspection */
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
$parentIDs[] = (int) $row['id'];
}
$query->closeCursor();
} while(count($parentIDs));
$rowCount = 0;
$multi = is_array($pageID) || $pageID instanceof PageArray;
if($multi && $recursive) {
// multiple page IDs combined with recursive option, must be handled individually
foreach($pageID as $id) {
$rowCount += $this->savePageStatus((int) "$id", $status, $recursive, $remove);
}
// exit early in this case
return $rowCount;
} else if($multi) {
// multiple page IDs without recursive option, can be handled in one query
$ids = array();
foreach($pageID as $id) {
$id = (int) "$id";
if($id > 0) $ids[$id] = $id;
}
if(!count($ids)) $ids[] = 0;
$query = $database->prepare("UPDATE pages SET status=$sql WHERE id IN(" . implode(',', $ids) . ")");
$database->execute($query);
return $query->rowCount();
} else {
// single page ID or Page object
$pageID = (int) "$pageID";
$query = $database->prepare("UPDATE pages SET status=$sql WHERE id=:page_id");
$query->bindValue(":page_id", $pageID, \PDO::PARAM_INT);
$database->execute($query);
$rowCount = $query->rowCount();
}
if(!$recursive) return $rowCount;
// recursive mode assumed from this point forward
$parentIDs = array($pageID);
do {
$parentID = array_shift($parentIDs);
// update all children to have the same status
$query = $database->prepare("UPDATE pages SET status=$sql WHERE parent_id=:parent_id");
$query->bindValue(":parent_id", $parentID, \PDO::PARAM_INT);
$database->execute($query);
$rowCount += $query->rowCount();
$query->closeCursor();
// locate children that themselves have children
$query = $database->prepare(
"SELECT pages.id FROM pages " .
"JOIN pages AS pages2 ON pages2.parent_id=pages.id " .
"WHERE pages.parent_id=:parent_id " .
"GROUP BY pages.id " .
"ORDER BY pages.sort"
);
$query->bindValue(':parent_id', $parentID, \PDO::PARAM_INT);
$database->execute($query);
/** @noinspection PhpAssignmentInConditionInspection */
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
$parentIDs[] = (int) $row['id'];
}
$query->closeCursor();
} while(count($parentIDs));
return $rowCount;
}
/**
@@ -1049,7 +1070,7 @@ class PagesEditor extends Wire {
}
/**
* 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)
@@ -1066,29 +1087,19 @@ class PagesEditor extends Wire {
if(is_string($options)) $options = Selectors::keyValueStringToArray($options);
if(!isset($options['recursionLevel'])) $options['recursionLevel'] = 0; // recursion level
if($parent === null) $parent = $page->parent;
if(isset($options['set']) && isset($options['set']['name'])) {
if(isset($options['set']) && isset($options['set']['name']) && strlen($options['set']['name'])) {
$name = $options['set']['name'];
} else {
// 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;
}
$name = $this->pages->names()->uniquePageName(array(
'name' => $page->name,
'parent' => $parent
));
}
$of = $page->of();
$page->of(false);
// Ensure all data is loaded for the page
foreach($page->fieldgroup as $field) {
@@ -1097,11 +1108,14 @@ 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;
$copy->parent = $parent;
$copy->of(false);
$copy->set('numChildren', 0);
// set any properties indicated in options
if(isset($options['set']) && is_array($options['set'])) {
foreach($options['set'] as $key => $value) {
@@ -1130,22 +1144,25 @@ class PagesEditor extends Wire {
if($copy->hasField($field)) $copy->trackChange($field->name);
}
$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);
$page->of($of);
throw $e;
}
$this->cloning = false;
$copy->setOutputFormatting($o);
$this->cloning--;
// 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);
$page->of($of);
return $this->pages->newNullPage();
}
// copy $page's files over to new page
if(PagefilesManager::hasFiles($page)) {
@@ -1182,6 +1199,9 @@ class PagesEditor extends Wire {
}
}
$copy->setQuietly('_cloning', null);
$copy->of($of);
$page->of($of);
$copy->resetTrackChanges();
$this->pages->cloned($page, $copy);
$this->pages->debugLog('clone', "page=$page, parent=$parent", $copy);
@@ -1237,6 +1257,37 @@ 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
*

File diff suppressed because it is too large Load Diff

View File

@@ -115,7 +115,7 @@ class PagesLoader extends Wire {
if(!empty($options['lazy'])) return false;
$value = false;
$filter = empty($options['findOne']);
$filter = empty($options['findAll']);
if(is_array($selector)) {
@@ -335,6 +335,7 @@ class PagesLoader extends Wire {
$pages = $this->pages->newPageArray($loadOptions);
}
$pageFinder->getPageArrayData($pages);
$pages->setTotal($total);
$pages->setLimit($limit);
$pages->setStart($start);
@@ -446,6 +447,7 @@ 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).
*
@@ -483,6 +485,7 @@ class PagesLoader extends Wire {
'joinFields' => array(),
'page' => null,
'pageClass' => '', // blank = auto detect
'pageArray' => null, // PageArray to populate to
'pageArrayClass' => 'PageArray',
'caller' => '',
);
@@ -639,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);
@@ -810,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) {

View File

@@ -189,7 +189,7 @@ 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) {

657
wire/core/PagesNames.php Normal file
View File

@@ -0,0 +1,657 @@
<?php namespace ProcessWire;
/**
* ProcessWire Pages Names
*
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
*/
class PagesNames extends Wire {
/**
* @var Pages
*
*/
protected $pages;
/**
* Name for untitled/temporary pages
*
* @var string
*
*/
protected $untitledPageName = 'untitled';
/**
* Delimiters that can separate words in page names
*
* @var array
*
*/
protected $delimiters = array('-', '_', '.');
/**
* Default delimiter for separating words in page names
*
* @var string
*
*/
protected $delimiter = '-';
/**
* Max length for page names
*
* @var int
*
*/
protected $nameMaxLength = 128;
/**
* Construct
*
* @param Pages $pages
*
*/
public function __construct(Pages $pages) {
$this->pages = $pages;
$pages->wire($this);
$untitled = $this->wire('config')->pageNameUntitled;
if($untitled) $this->untitledPageName = $untitled;
$this->nameMaxLength = Pages::nameMaxLength;
parent::__construct();
}
/**
* Assign a name to given Page (if it doesnt already have one)
*
* @param Page $page
* @param string $format
* @return string Returns page name that was assigned
* @throws WireException
*
*/
public function setupNewPageName(Page $page, $format = '') {
$pageName = $page->name;
// check if page already has a non-“untitled” name assigned that we should leave alone
if(strlen($pageName) && !$this->isUntitledPageName($pageName)) return '';
// determine what format should be used for the generated page name
if(!strlen($format)) $format = $this->defaultPageNameFormat($page);
// generate a page name from determined format
$pageName = $this->pageNameFromFormat($page, $format);
// ensure page name is unique
$pageName = $this->uniquePageName($pageName, $page);
// assign to page
$page->name = $pageName;
// indicate that page has auto-generated name for savePageQuery (provides adjustName behavior for new pages)
$page->setQuietly('_hasAutogenName', $pageName);
return $pageName;
}
/**
* Does the given page have an auto-generated name (during this request)?
*
* @param Page $page
* @return string|bool Returns auto-generated name if present, or boolean false if not
*
*/
public function hasAutogenName(Page $page) {
$name = $page->get('_hasAutogenName');
if(empty($name)) $name = false;
return $name;
}
/**
* Is given page name an untitled page name?
*
* @param string $name
* @return bool
*
*/
public function isUntitledPageName($name) {
list($namePrefix,) = $this->nameAndNumber($name);
return $namePrefix === $this->untitledPageName;
}
/**
* If given name has a numbered suffix, return array with name (excluding suffix) and the numbered suffix
*
* Returns array like `[ 'name', 123 ]` where `name` is name without the suffix, and `123` is the numbered suffix.
* If the name did not have a numbered suffix, then the 123 will be 0 and `name` will be the given `$name`.
*
* @param string $name
* @param string $delimiter Character(s) that separate name and numbered suffix
* @return array
*
*/
public function nameAndNumber($name, $delimiter = '') {
if(empty($delimiter)) $delimiter = $this->delimiter;
$fail = array($name, 0);
if(strpos($name, $delimiter) === false) return $fail;
$parts = explode($delimiter, $name);
$suffix = array_pop($parts);
if(!ctype_digit($suffix)) return $fail;
$suffix = ltrim($suffix, '0');
return array(implode($delimiter, $parts), (int) $suffix);
}
/**
* Does the given name or Page have a number suffix? Returns the number if yes, or false if not
*
* @param string|Page $name
* @param bool $getNamePrefix Return the name prefix rather than the number suffix? (default=false)
* @return int|bool|string Returns false if no number suffix, or int for number suffix or string for name prefix (if requested)
*
*/
public function hasNumberSuffix($name, $getNamePrefix = false) {
if($name instanceof Page) $name = $name->name;
list($namePrefix, $numberSuffix) = $this->nameAndNumber($name);
if(!$numberSuffix) return false;
return $getNamePrefix ? $namePrefix : $numberSuffix;
}
/**
* Get the name format string that should be used for given $page if no name was assigned
*
* @param Page $page
* @param array $options
* - `fallbackFormat` (string): Fallback format if another cannot be determined (default='untitled-time').
* - `parent` (Page|null): Optional parent page to use instead of $page->parent (default=null).
* @return string
*
*/
public function defaultPageNameFormat(Page $page, array $options = array()) {
$defaults = array(
'fallbackFormat' => 'untitled-time',
'parent' => null,
);
$options = array_merge($defaults, $options);
$parent = $options['parent'] ? $options['parent'] : $page->parent();
$format = '';
if($parent && $parent->id && $parent->template->childNameFormat) {
// if format specified with parent template, use that
$format = $parent->template->childNameFormat;
} else if(strlen("$page->title")) {
// default format is title (when the page has one)
$format = 'title';
} else if($this->wire('languages') && $page->title instanceof LanguagesValueInterface) {
// check for multi-language title
/** @var LanguagesPageFieldValue $pageTitle */
$pageTitle = $page->title;
if(strlen($pageTitle->getDefaultValue())) $format = 'title';
}
if(empty($format)) {
if($page->id && $options['fallbackFormat']) {
$format = $options['fallbackFormat'];
} else {
$format = 'untitled-time';
}
}
return $format;
}
/**
* Create a page name from the given format
*
* - Returns a generated page name that is not yet assigned to the page.
* - If no format is specified, it first falls back to page parent template `childNameFormat` property (if present).
* - If no format can be determined, it falls back to a randomly generated page name.
* - Does not check if page name is already in use.
*
* Options for $format argument:
*
* - `title` Build name based on “title” field.
* - `field` Build name based on any other field name you choose, replace “field” with any field name.
* - `text` Text already in the right format (thats not a field name) will be used literally, replace “text” with your text.
* - `random` Randomly generates a name.
* - `untitled` Uses an auto-incremented “untitled” name.
* - `untitled-time` Uses an “untitled” name followed by date/time number string.
* - `a|b|c` Builds name from first matching field name, where a|b|c are your field names.
* - `{field}` Builds name from the given field name.
* - `{a|b|c}` Builds name first matching field name, where a|b|c would be replaced with your field names.
* - `date:Y-m-d-H-i` Builds name from current date - replace “Y-m-d-H-i” with desired wireDate() format.
* - `string with space` A string that does not match one of the above and has space is assumed to be a wireDate() format.
* - `string with /` A string that does not match one of the above and has a “/” slash is assumed to be a wireDate() format.
*
* For formats above that accept a wireDate() format, see `WireDateTime::date()` method for format details. It accepts PHP
* date() format, PHP strftime() format, as well as some other predefined options.
*
* @param Page $page
* @param string|array $format Optional format. If not specified, pulls from $pages parent template.
* @param array $options Options to modify behavior. May also be specified in $format argument.
* - `language` (Language|string): Language to use
* - `format` (string): Optional format to use, if $options were specified in $format argument.
* @return string
*
*/
public function pageNameFromFormat(Page $page, $format = '', array $options = array()) {
$defaults = array(
'format' => '',
'language' => null,
);
if(is_array($format)) {
$options = $format;
$format = empty($options['format']) ? '' : $options['format'];
}
$options = array_merge($defaults, $options);
if(!strlen($format)) $format = $this->defaultPageNameFormat($page);
$format = trim($format);
$name = '';
if($options['language']) {
$this->wire('languages')->setLanguage($options['language']);
}
if($format === 'title' && !strlen(trim((string) $page->title))) {
$format = 'untitled-time';
}
if($format === 'title') {
// title
$name = trim((string) $page->title);
} else if($format === 'random') {
// globally unique randomly generated page name
$name = $this->uniqueRandomPageName();
} else if($format === 'untitled') {
// just untitled
$name = $this->untitledPageName();
} else if($format === 'untitled-time') {
// untitled with datetime, i.e. “untitled-0yymmddhhmmss” (note leading 0 differentiates from increment)
$dateStr = date('ymdHis');
$name = $this->untitledPageName() . '-0' . $dateStr;
} else if(strpos($format, '}')) {
// string with {field_name} to text
$name = $page->getText($format, true, false);
} else if(strpos($format, '|')) {
// field names separated by "|" until one matches
$name = $page->getUnformatted($format);
} else if(strpos($format, 'date:') === 0) {
// specified date format
list(, $format) = explode('date:', $format);
if(empty($format)) $format = 'Y-m-d H:i:s';
$name = wireDate(trim($format));
} else if(strpos($format, ' ') !== false || strpos($format, '/') !== false) {
// date assumed when spaces or slashes present in format
$name = wireDate($format);
} else if($this->wire('sanitizer')->fieldName($format) === $format) {
// single field name or predefined string
// this can also return null, which falls back to if() statement below
$name = (string) $page->getUnformatted($format);
}
if(!strlen($name)) {
// predefined string that is not a field name
$name = $format;
}
if(strlen($name) > $this->nameMaxLength) $name = $this->adjustNameLength($name);
$utf8 = $this->wire('config')->pageNameCharset === 'UTF8';
$sanitizer = $this->wire('sanitizer');
$name = $utf8 ? $sanitizer->pageNameUTF8($name) : $sanitizer->pageName($name, Sanitizer::translate);
if($options['language']) {
$this->wire('languages')->unsetLanguage();
}
return $name;
}
/**
* Get a unique page name
*
* 1. If given no arguments, it returns a random globally unique page name.
* 2. If given just a $name, it returns that name (if globally unique), or an incremented version of it that is globally unique.
* 3. If given both $page and $name, it returns given name if unique in parent, or incremented version that is.
* 4. If given just a $page, the name is pulled from $page and behavior is the same as #3 above.
*
* The returned value is not yet assigned to the given $page, so if it is something different than what
* is already on $page, youll want to assign it manually after this.
*
* @param string|Page|array $name Name to make unique
* You may optionally substitute the $page argument or $options arguments here, if that is all you need.
* @param Page||string|null|array Page to exclude from duplicate check and/or to pull $name or parent from (if not otherwise specified).
* Note that specifying a Page is important if the page already exists, as it is used as the page to exclude when checking for
* name collisions, and we want to exclude $page from that check. You may optionally substitute the $options or $name arguments
* here, if that is all you need. If $parent or $name are specified separately from this $page argument, they will override
* whatever parent or name settings are on this $page argument.
* @param array $options
* - `parent` (Page|null): Optionally specify a different parent if $page does not currently have the parent you want to use.
* - `language` (Language|int): Get unique for this language (if multi-language page names active).
* - `page` (Page|null): If you specified no $page argument, you can optionally bundle it in the $options array.
* - `name` (string): If you specified no $name argument, you can optionally bundle it in the $options array.
* @return string Returns unique name
*
*/
public function uniquePageName($name = '', $page = null, array $options = array()) {
$defaults = array(
'name' => '',
'page' => null,
'parent' => null,
'language' => null
);
// handle argument substitutions
if(is_array($page)) {
// options specified in $page argument
$options = $page;
$page = !empty($options['page']) ? $options['page'] : null;
} else if(is_array($name)) {
// options specified in $name argument
$options = $name;
$name = !empty($options['name']) ? $options['name'] : '';
} else if($name instanceof Page) {
// $page argument specified in $name argument
$_name = is_string($page) ? $page : '';
$page = $name;
$name = $_name;
}
$options = array_merge($defaults, $options);
if(empty($page) && !empty($options['page'])) $page = $options['page'];
if(empty($name) && !empty($options['name'])) $name = $options['name'];
if($page) {
if($options['parent'] === null) $options['parent'] = $page->parent();
if(!strlen($name)) {
if($options['language']) {
$name = $page->get("name$options[language]");
if(!strlen($name)) $name = $page->name;
} else {
$name = $page->name;
}
}
$options['page'] = $page;
}
if(!strlen($name)) {
// no name currently present, so we need to determine what kind of name it should have
if($page) {
$format = $this->defaultPageNameFormat($page, array(
'fallbackFormat' => $page->id ? 'random' : 'untitled-time',
'parent' => $options['parent']
));
$name = $this->pageNameFromFormat($page, $format, array('language' => $options['language']));
} else {
$name = $this->uniqueRandomPageName();
}
}
while($this->pageNameExists($name, $options)) {
$name = $this->incrementName($name);
}
if(strlen($name) > $this->nameMaxLength) $name = $this->adjustNameLength($name);
return $name;
}
/**
* If name exceeds maxLength, truncate it, while keeping any numbered suffixes in place
*
* @param string $name
* @param int $maxLength
* @return string
*
*/
public function adjustNameLength($name, $maxLength = 0) {
if($maxLength < 1) $maxLength = $this->nameMaxLength;
if(strlen($name) <= $maxLength) return $name;
$trims = implode('', $this->delimiters);
$pos = 0;
list($namePrefix, $numberSuffix) = $this->nameAndNumber($name);
if($namePrefix !== $name) {
$numberSuffix = $this->delimiter . $numberSuffix;
$maxLength -= strlen($numberSuffix);
} else {
$numberSuffix = '';
}
if(strlen($namePrefix) > $maxLength) {
$namePrefix = substr($namePrefix, 0, $maxLength);
}
// find word delimiter closest to end of string
foreach($this->delimiters as $c) {
$p = strrpos($namePrefix, $c);
if((int) $p > $pos) $pos = $p;
}
// use word delimiter pos as maxLength when its relatively close to the end
if(!$pos || $pos < (strlen($namePrefix) / 1.3)) $pos = $maxLength;
$name = substr($namePrefix, 0, $pos);
$name = rtrim($name, $trims);
// append number suffix if there was one
if($numberSuffix) $name .= $numberSuffix;
return $name;
}
/**
* Increment the suffix of a page name, or add one if not present
*
* @param string $name
* @param int|null $num Number to use, or omit to determine and increment automatically
* @return string
*
*/
public function incrementName($name, $num = null) {
list($namePrefix, $n) = $this->nameAndNumber($name);
if($namePrefix !== $name) {
// name already had an increment
if($num) {
// specific number was supplied
$num = (int) $num;
$name = $namePrefix . $this->delimiter . $num;
} else {
// no number supplied
// make sure that any leading zeros are retained before we increment number
$zeros = '';
while(strpos($name, $namePrefix . $this->delimiter . "0$zeros") === 0) $zeros .= '0';
// increment the number
$name = $namePrefix . $this->delimiter . $zeros . (++$n);
}
} else {
// name does not yet have an increment, so make it "name-1"
if(!is_int($num) || $num < 1) $num = 1;
$name = $namePrefix . $this->delimiter . $num;
}
return $this->adjustNameLength($name);
}
/**
* Is the given name is use by a page?
*
* If the `multilang` option is used, it checks if the page name exists in any language.
* IF the `language` option is used, it only checks that particular language (regardless of `multilang` option).
*
* @param string $name
* @param array $options
* - `page` (Page|int): Ignore this Page or page ID
* - `parent` (Page|int): Limit search to only this parent.
* - `multilang` (bool): Check other languages if multi-language page names supported? (default=false)
* - `language` (Language|int): Limit check to only this language (default=null)
* @return int Returns quantity of pages using name, or 0 if name not in use.
*
*/
public function pageNameExists($name, array $options = array()) {
$defaults = array(
'page' => null,
'parent' => null,
'language' => null,
'multilang' => false,
);
$options = array_merge($defaults, $options);
$languages = $options['multilang'] || $options['language'] ? $this->wire('languages') : null;
if($languages && !$this->wire('modules')->isInstalled('LanguageSupportPageNames')) $languages = null;
if($this->wire('config')->pageNameCharset == 'UTF8') {
$name = $this->wire('sanitizer')->pageName($name, Sanitizer::toAscii);
}
$wheres = array();
$binds = array();
$parentID = $options['parent'] === null ? null : (int) "$options[parent]";
$pageID = $options['page'] === null ? null : (int) "$options[page]";
if($languages) {
foreach($languages as $language) {
if($options['language'] && "$options[language]" !== "$language") continue;
$property = $language->isDefault() ? "name" : "name" . (int) $language->id;
$wheres[] = "$property=:$property";
$binds[":$property"] = $name;
}
$wheres = array('(' . implode(' OR ', $wheres) . ')');
} else {
$wheres[] = 'name=:name';
$binds[':name'] = $name;
}
if($parentID) {
$wheres[] = 'parent_id=:parent_id';
$binds[':parent_id'] = $parentID;
}
if($pageID) {
$wheres[] = 'id!=:id';
$binds[':id'] = $pageID;
}
$sql = 'SELECT COUNT(*) FROM pages WHERE ' . implode(' AND ', $wheres);
$query = $this->wire('database')->prepare($sql);
foreach($binds as $key => $value) {
$query->bindValue($key, $value);
}
$query->execute();
$qty = (int) $query->fetchColumn();
$query->closeCursor();
return $qty;
}
/**
* Get a random, globally unique page name
*
* @param array $options
* - `page` (Page): If name is or should be assigned to a Page, specify it here. (default=null)
* - `length` (int): Required/fixed length, or omit for random length (default=0).
* - `min` (int): Minimum required length, if fixed length not specified (default=6).
* - `max` (int): Maximum allowed length, if fixed length not specified (default=min*2).
* - `alpha` (bool): Include alpha a-z letters? (default=true)
* - `numeric` (bool): Include numeric digits 0-9? (default=true)
* - `confirm` (bool): Confirm that name is globally unique? (default=true)
* - `parent` (Page|int): If specified, name must only be unique for this parent Page or ID (default=0).
* - `prefix` (string): Prepend this prefix to page name (default='').
* - `suffix` (string): Append this suffix to page name (default='').
*
* @return string
*
*/
public function uniqueRandomPageName($options = array()) {
$defaults = array(
'page' => null,
'length' => 0,
'min' => 6,
'max' => 0,
'alpha' => true,
'numeric' => true,
'confirm' => true,
'parent' => 0,
'prefix' => '',
'suffix' => '',
);
if(is_int($options)) $options = array('length' => $options);
$options = array_merge($defaults, $options);
$rand = new WireRandom();
$this->wire($rand);
do {
if($options['length'] < 1) {
if($options['min'] < 1) $options['min'] = 6;
if($options['max'] < $options['min']) $options['max'] = $options['min'] * 2;
if($options['min'] == $options['max']) {
$length = $options['max'];
} else {
$length = mt_rand($options['min'], $options['max']);
}
} else {
$length = (int) $options['length'];
}
if($options['alpha'] && $options['numeric']) {
$name = $rand->alphanumeric($length, array('upper' => false, 'noStart' => '0123456789'));
} else if($options['numeric']) {
$name = $rand->numeric($length);
} else {
$name = $rand->alpha($length);
}
$name = $options['prefix'] . $name . $options['suffix'];
if($options['confirm']) {
$qty = $this->pageNameExists($name, array('page' => $options['page']));
} else {
$qty = 0;
}
} while($qty);
if($options['page'] instanceof Page) $options['page']->set('name', $name);
return $name;
}
/**
* Return the untitled page name string
*
* @return string
*
*/
public function untitledPageName() {
return $this->untitledPageName;
}
}

View File

@@ -5,7 +5,7 @@
*
* Implements page trash/restore/empty methods of the $pages API variable
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
*/
@@ -17,7 +17,13 @@ class PagesTrash extends Wire {
*
*/
protected $pages;
/**
* Construct
*
* @param Pages $pages
*
*/
public function __construct(Pages $pages) {
$this->pages = $pages;
}
@@ -34,13 +40,17 @@ class PagesTrash extends Wire {
*
*/
public function trash(Page $page, $save = true) {
if(!$this->pages->isDeleteable($page) || $page->template->noTrash) {
throw new WireException("This page may not be placed in the trash");
}
if(!$trash = $this->pages->get($this->config->trashPageID)) {
throw new WireException("Unable to load trash page defined by config::trashPageID");
}
$page->addStatus(Page::statusTrash);
if(!$page->parent->isTrash()) {
$parentPrevious = $page->parent;
$page->parent = $trash;
@@ -49,7 +59,10 @@ class PagesTrash extends Wire {
} else {
$parentPrevious = null;
}
if(!preg_match('/^' . $page->id . '(\.\d+\.\d+)?_.+/', $page->name)) {
$nameInfo = $this->parseTrashPageName($page->name);
if(!$nameInfo || $nameInfo['id'] != $page->id) {
// make the name unique when in trash, to avoid namespace collision and maintain parent restore info
$name = $page->id;
if($parentPrevious && $parentPrevious->id) {
@@ -68,12 +81,13 @@ class PagesTrash extends Wire {
$page->set("name$language->id", $name . "_" . $langName);
}
}
}
if($save) $this->pages->save($page);
$this->pages->editor()->savePageStatus($page->id, Page::statusTrash, true, false);
$this->pages->trashed($page);
if($save) $this->pages->trashed($page);
$this->pages->debugLog('trash', $page, true);
return true;
}
@@ -90,55 +104,186 @@ class PagesTrash extends Wire {
*/
public function restore(Page $page, $save = true) {
if(preg_match('/^(' . $page->id . ')((?:\.\d+\.\d+)?)_(.+)$/', $page->name, $matches)) {
if($matches[2]) {
/** @noinspection PhpUnusedLocalVariableInspection */
list($unused, $parentID, $sort) = explode('.', $matches[2]);
$parentID = (int) $parentID;
$sort = (int) $sort;
} else {
$parentID = 0;
$sort = 0;
}
$prefix = $matches[1] . $matches[2] . '_';
$name = $matches[3];
if($parentID && $page->parent->isTrash() && !$page->parentPrevious) {
// no new parent was defined, so use the one in the page name
$newParent = $this->pages->get($parentID);
if($newParent->id && $newParent->id != $page->id) {
$page->parent = $newParent;
$page->sort = $sort;
}
}
if(!count($page->parent->children("name=$name, include=all"))) {
$page->name = $name; // remove namespace collision info if no collision
// do the same for other languages, when applicable
if($this->wire('languages') && $this->wire('modules')->isInstalled('LanguageSupportPageNames')) {
foreach($this->wire('languages') as $language) {
if($language->isDefault()) continue;
$langName = $page->get("name$language->id");
if(strpos($langName, $prefix) !== 0) continue;
$langName = str_replace($prefix, '', $langName);
$page->set("name$language->id", $langName);
}
}
}
}
if(!$page->parent->isTrash()) {
$info = $this->getRestoreInfo($page, true);
if(!$info['restorable']) return false;
if($page->parent->isTrash()) {
if($save) $page->save();
} else {
$page->removeStatus(Page::statusTrash);
if($save) $page->save();
$this->pages->editor()->savePageStatus($page->id, Page::statusTrash, true, true);
$this->pages->restored($page);
if($save) $this->pages->restored($page);
$this->pages->debugLog('restore', $page, true);
}
return true;
}
/**
* Get info needed to restore a Page that is in the trash
*
* Returns array with the following info:
* - `restorable` (bool): Is the page restorable to a previous known/existing parent?
* - `notes` (array): Any additional notes to explain restore info (like reason why not restorable, or why name changed, etc.)
* - `parent` (Page|NullPage): Parent page that it should restore to
* - `parent_id` (int): ID of parent page that it should restore to
* - `sort` (int): Sort order that should be restored to page
* - `name` (string): Name that should be restored to pages “name” property.
* - `namePrevious` (string): Previous name, if we had to modify the original name to make it restorable.
* - `name{id}` (string): Name that should be restored to language where {id} is language ID (if appliable).
*
* @param Page $page Page to restore
* @param bool $populateToPage Populate this information to given page? (default=false)
* @return array
*
*/
public function getRestoreInfo(Page $page, $populateToPage = false) {
$info = array(
'restorable' => false,
'notes' => array(),
'parent' => $this->pages->newNullPage(),
'parent_id' => 0,
'sort' => 0,
'name' => '',
'namePrevious' => '',
);
/** @var Languages|array $languages */
$languages = $this->wire('languages');
if(!$languages || !$this->wire('modules')->isInstalled('LanguageSupportPageNames')) $languages = array();
// initialize name properties in $info for each language
foreach($languages as $language) {
$info["name$language->id"] = '';
}
$result = $this->parseTrashPageName($page->name);
if(!$result || $result['id'] !== $page->id) {
// page does not have restore info
$info['notes'][] = 'Page name does not contain restore information';
return $info;
}
$name = $result['name'];
$trashPrefix = $result['prefix']; // pageID.parentID.sort_ prefix for testing other language names later
$newParent = null;
$parentID = $result['parent_id'];
$sort = $result['sort'];
if($parentID && $parentID != $page->id) {
if($page->rootParent()->isTrash()) {
// no new parent was defined, so use the one in the page name
$newParent = $this->pages->get($parentID);
if(!$newParent->id) {
$newParent = null;
$info['notes'][] = 'Original parent no longer exists';
}
} else {
$info['notes'][] = 'Page root parent is not trash';
}
} else if($parentID) {
$info['notes'][] = "Invalid parent ID: $parentID";
} else {
if($save) $page->save();
// page was likely trashed a long time ago, before this info was stored
$info['notes'][] = 'Page name does not contain previous parent or sort info';
}
return true;
$info['parent'] = $newParent ? $newParent : $this->pages->newNullPage();
$info['parent_id'] = $parentID;
$info['sort'] = $sort;
// if we have no new parent available we can exit now
if(!$newParent) {
$info['notes'][] = 'Unable to determine parent to restore to';
return $info;
}
// check if there is already a page at the restore location with the same name
$namePrevious = $name;
$name = $this->pages->names()->uniquePageName($name, $page, array('parent' => $newParent));
if($name !== $namePrevious) {
$info['notes'][] = "Name changed from '$namePrevious' to '$name' to be unique in new parent";
$info['namePrevious'] = $namePrevious;
}
$info['name'] = $name;
$info['restorable'] = true;
if($populateToPage) {
$page->name = $name;
$page->parent = $newParent;
$page->sort = $sort;
}
// do the same for other languages, when applicable
foreach($languages as $language) {
/** @var Language $language */
if($language->isDefault()) continue;
$langName = $page->get("name$language->id");
if(!strlen($langName)) continue;
if(strpos($langName, $trashPrefix) === 0) {
list(,$langName) = explode('_', $langName);
}
$langNamePrevious = $langName;
$langName = $this->pages->names()->uniquePageName($langName, $page, array(
'parent' => $newParent,
'language' => $language
));
if($populateToPage) $page->set("name$language->id", $langName);
$info["name$language->id"] = $langName;
if($langName !== $langNamePrevious) {
$info['notes'][] = $language->get('title|name') . ' ' .
"name changed from '$langNamePrevious' to '$langName' to be unique in new parent";
}
}
return $info;
}
/**
* Parse a trashed page name into an array of its components
*
* @param string $name
* @return array|bool Returns array of info if name is a trash/restore name, or boolean false if not
*
*/
public function parseTrashPageName($name) {
$info = array(
'id' => 0,
'parent_id' => 0,
'sort' => 0,
'name' => $name,
'prefix' => '',
'note' => '',
);
// match "pageID.parentID.sort_name" in page name (1).(2.2)_3
if(!preg_match('/^(\d+)((?:\.\d+\.\d+)?)_(.+)$/', $name, $matches)) return false;
$info['id'] = (int) $matches[1];
$info['name'] = $matches[3];
if($matches[2]) {
// matches[2] contains ".parentID.sort"
list(, $parentID, $sort) = explode('.', $matches[2]);
$info['parent_id'] = (int) $parentID;
$info['sort'] = (int) $sort;
} else {
// page was likely trashed a long time ago, before this info was stored
$info['note'] = 'Page name does not contain previous parent or sort info';
}
// pageID.parentID.sort_ prefix that can be used with other language names
$info['prefix'] = $matches[1] . $matches[2] . '_';
return $info;
}
/**
@@ -146,54 +291,259 @@ class PagesTrash extends Wire {
*
* Populates error notices when there are errors deleting specific pages.
*
* @return int Returns total number of pages deleted from trash.
* This number is negative or 0 if not all pages could be deleted and error notices may be present.
* @param array $options
* - `chunkSize` (int): Pages will be deleted in chunks of this many pages per chunk (default=100).
* - `chunkTimeLimit` (int): Maximum seconds allowed to process deletion of each chunk (default=600).
* - `chunkLimit' (int): Maximum chunks to process in an emptyTrash() call (default=1000);
* - `pageLimit` (int): Maximum pages to delete per emptyTrash() call (default=0, no limit).
* - `timeLimit` (int): Maximum time (in seconds) to allow for trash empty (default=3600).
* - `pass2` (bool): Perform a secondary pass using alternate method as a backup? (default=true)
* Note: pass2 is always disabled when a pageLimit is in use or timeLimit has been exceeded.
* - `verbose` (bool): Return verbose array of information about the trash empty process? For debug/dev purposes (default=false)
* @return int|array Returns integer (default) or array in verbose mode.
* - By default, returns total number of pages deleted from trash. This number is negative or 0 if not
* all pages could be deleted and error notices may be present.
* - Returns associative array with verbose information if verbose option is chosen.
*
*/
public function emptyTrash() {
public function emptyTrash(array $options = array()) {
$trashPage = $this->pages->get($this->wire('config')->trashPageID);
$selector = "include=all, has_parent=$trashPage, children.count=0, status=" . Page::statusTrash;
$defaults = array(
'chunkSize' => 100,
'chunkTimeLimit' => 600,
'chunkLimit' => 100,
'pageLimit' => 0,
'timeLimit' => 3600,
'pass2' => true,
'verbose' => false,
);
$options = array_merge($defaults, $options);
$trashPage = $this->getTrashPage();
$masterSelector = "include=all, children.count=0, status=" . Page::statusTrash;
$totalDeleted = 0;
$lastTotalInTrash = 0;
$numBatches = 0;
$chunkCnt = 0;
$errorCnt = 0;
$nonTrashIDs = array(); // page IDs that had trash status but did not have trash parent
$result = array();
$timer = $options['verbose'] ? Debug::timer() : null;
$startTime = time();
$stopTime = $options['timeLimit'] ? $startTime + $options['timeLimit'] : false;
$stopNow = false;
$database = $this->wire('database');
$useTransaction = $database->supportsTransaction();
$options['stopTime'] = $stopTime; // for pass2
$timeExpired = false;
$onlyDirectChildren = true; // limit to direct children at first
if($options['chunkTimeLimit'] > $options['timeLimit']) {
$options['chunkTimeLimit'] = $options['timeLimit'];
}
// Empty trash pass1:
// Operates by finding pages in trash using Page::statusTrash that have no children
do {
set_time_limit(60 * 10);
$totalInTrash = $this->pages->count($selector);
if(!$totalInTrash || $totalInTrash == $lastTotalInTrash) break;
$lastTotalInTrash = $totalInTrash;
$items = $this->pages->find("$selector, limit=100");
$cnt = $items->count();
$selector = $masterSelector;
if($options['chunkTimeLimit']) {
set_time_limit($options['chunkTimeLimit']);
}
if(count($nonTrashIDs)) {
$selector .= ", id!=" . implode('|', $nonTrashIDs);
}
if($onlyDirectChildren) {
// limit to direct children of trash page that themselves have no children
$selector .= ", parent_id=$trashPage->id";
} else {
$totalInTrash = $this->pages->count($selector);
if(!$totalInTrash || $totalInTrash == $lastTotalInTrash) break;
$lastTotalInTrash = $totalInTrash;
}
if($options['chunkSize'] > 0) {
$selector .= ", limit=$options[chunkSize]";
}
$items = $this->pages->find($selector);
$numItems = $items->count();
$totalItems = $items->getTotal();
$numDeleted = 0;
if($useTransaction) $database->beginTransaction();
foreach($items as $item) {
// determine if any limits have been reached
if($stopTime && time() > $stopTime) {
$stopNow = true;
$timeExpired = true;
}
if($options['pageLimit'] && $totalDeleted >= $options['pageLimit']) {
$stopNow = true;
}
if($stopNow) break;
// if page does not have trash as a parent, then this is a page with trash status
// that is somewhere else in the page tree (not likely)
if(!$onlyDirectChildren && $item->rootParent()->id !== $trashPage->id) {
$nonTrashIDs[$item->id] = $item->id;
$errorCnt++;
continue;
}
// delete the page
try {
$totalDeleted += $this->pages->delete($item, true);
$numDeleted += $this->pages->delete($item, true);
} catch(\Exception $e) {
$this->error($e->getMessage());
$errorCnt++;
}
}
$this->pages->uncacheAll();
$numBatches++;
} while($cnt);
// just in case anything left in the trash, use a backup method
$trashPage = $this->pages->get($trashPage->id); // fresh copy
$trashPages = $trashPage->children("include=all");
foreach($trashPages as $t) {
try {
$totalDeleted += $this->pages->delete($t, true);
} catch(\Exception $e) {
$this->error($e->getMessage());
$totalDeleted += $numDeleted;
if($useTransaction) $database->commit();
$this->pages->uncacheAll();
if($options['chunkLimit'] && $chunkCnt >= $options['chunkLimit']) {
// if chunk limit exceeded then stop now
$stopNow = true;
} else if($onlyDirectChildren) {
// move past direct children next if all were loaded in this chunk
if($totalItems === $numItems || !$numDeleted) $onlyDirectChildren = false;
} else if(!$numDeleted) {
// if no items deleted (and we're beyond direct children), we should stop now
$stopNow = true;
}
if(!$stopNow) $chunkCnt++;
} while(!$stopNow);
// if recording verbose info, populate it for pass1 now
if($options['verbose']) {
$result['pass1_cnt'] = $chunkCnt;
$result['pass1_numDeleted'] = $totalDeleted;
$result['pass1_numErrors'] = $errorCnt;
$result['pass1_elapsedTime'] = Debug::timer($timer);
$result['pass1_timeExpired'] = $timeExpired;
}
if(count($nonTrashIDs)) {
// remove trash status from the pages that should not have it
$this->pages->editor()->savePageStatus($nonTrashIDs, Page::statusTrash, false, true);
}
$this->pages->uncacheAll();
if($totalDeleted) {
$totalInTrash = $this->pages->count("has_parent=$trashPage, include=all, status=" . Page::statusTrash);
if($totalInTrash) $totalDeleted = $totalDeleted * -1;
// Empty trash pass2:
// Operates by finding pages that are children of the Trash and performing recursive delete upon them
if($options['pass2'] && !$stopNow && !$options['pageLimit']) {
if($useTransaction) $database->beginTransaction();
$totalDeleted += $this->emptyTrashPass2($options, $result);
if($useTransaction) $database->commit();
}
if($totalDeleted || $options['verbose']) {
$numTrashChildren = $this->wire('pages')->trasher()->getTrashTotal();
// return a negative number if pages still remain in trash
if($numTrashChildren && !$options['verbose']) $totalDeleted = $totalDeleted * -1;
} else {
$numTrashChildren = 0;
}
if($options['verbose']) {
$result['startTime'] = $startTime;
$result['elapsedTime'] = Debug::timer($timer);
$result['pagesPerSecond'] = $totalDeleted ? round($totalDeleted / $result['elapsedTime'], 2) : 0;
$result['timeExpired'] = !empty($result['pass1_timeExpired']) || !empty($result['pass2_timeExpired']);
$result['numDeleted'] = $totalDeleted;
$result['numRemain'] = $numTrashChildren;
$result['numErrors'] = $errorCnt;
$result['numMispaced'] = count($nonTrashIDs);
$result['idsMisplaced'] = $nonTrashIDs;
$result['options'] = $options;
return $result;
}
return $totalDeleted;
}
/**
* Secondary pass for trash deletion
*
* This works by finding the children of the trash page and performing a recursive delete on them.
*
* @param array $options Options passed to emptyTrash() method
* @param array $result Verbose array, modified directly
* @return int
*
*/
protected function emptyTrashPass2(array $options, &$result) {
if($options['chunkTimeLimit']) {
set_time_limit($options['chunkTimeLimit']);
}
$timer = $options['verbose'] ? Debug::timer() : null;
$numErrors = 0;
$numDeleted = 0;
$timeExpired = false;
$trashPage = $this->getTrashPage();
$trashPages = $trashPage->children("include=all");
foreach($trashPages as $t) {
try {
// perform recursive delete
$numDeleted += $this->pages->delete($t, true);
} catch(\Exception $e) {
$this->error($e->getMessage());
$numErrors++;
}
if($options['stopTime'] && time() > $options['stopTime']) {
$timeExpired = true;
break;
}
}
$this->pages->uncacheAll();
if($options['verbose']) {
$result['pass2_numDeleted'] = $numDeleted;
$result['pass2_numErrors'] = $numErrors;
$result['pass2_elapsedTime'] = Debug::timer($timer);
$result['pass2_timeExpired'] = $timeExpired;
}
return $numDeleted;
}
/**
* Get total number of pages in trash
*
* @return int
*
*/
public function getTrashTotal() {
return $this->pages->count("include=all, status=" . Page::statusTrash);
}
/**
* Return the root parent trash page
*
* @return Page
* @throws WireException if trash page cannot be located (highly unlikely)
*
*/
public function getTrashPage() {
$trashPageID = $this->wire('config')->trashPageID;
$trashPage = $this->pages->get((int) $trashPageID);
if(!$trashPage->id || $trashPage->id != $trashPageID) {
throw new WireException("Cannot find trash page $trashPageID");
}
return $trashPage;
}
}

View File

@@ -282,6 +282,8 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
if(empty($options['caller'])) {
$caller = $this->className() . ".get($selectorString)";
$options['caller'] = $caller;
} else {
$caller = $options['caller'];
}
if(ctype_digit("$selectorString")) {
@@ -547,10 +549,11 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
/**
* Hook called just before a page is saved
*
* #pw-hooker
* #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) {
@@ -564,11 +567,12 @@ 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-hooker
* #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()) { }
@@ -576,9 +580,10 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
/**
* Hook called when a new page has been added
*
* #pw-hooker
* #pw-internal
*
* @param Page $page
* @deprecated
*
*/
public function ___added(Page $page) { }
@@ -586,17 +591,18 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
/**
* Hook called when a page is about to be deleted, but before data has been touched
*
* #pw-hooker
* #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-hooker
* #pw-internal
*
* @param Page $page
*

View File

@@ -15,6 +15,8 @@
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* https://processwire.com
*
* @method string renderPager(array $options = array()) Renders pagination, when MarkupPageArray module installed
*
*/
@@ -136,6 +138,45 @@ class PaginatedArray extends WireArray implements WirePaginatable {
return $this->numStart;
}
/**
* Does this WireArray have more than one pagination?
*
* #pw-group-other
*
* @return bool
* @since 3.0.120
*
*/
public function hasPagination() {
return $this->getTotal() > 0 && $this->count() < $this->getTotal();
}
/**
* Is there a next pagination containing more items in this PaginatedArray after the current one?
*
* #pw-group-other
*
* @return int
* @since 3.0.120
*
*/
public function hasNextPagination() {
return $this->getStart() + $this->count() < $this->getTotal();
}
/**
* Is there a previous pagination before the current one?
*
* #pw-group-other
*
* @return bool
* @since 3.0.120
*
*/
public function hasPrevPagination() {
return $this->getStart() > 0;
}
/**
* Get a property of the PageArray
*
@@ -175,35 +216,62 @@ class PaginatedArray extends WireArray implements WirePaginatable {
*
* This returns a string of `1 to 10 of 30` (items) or `1 of 10` (pages) for example.
*
* You can optionally replace either of the arguments with an $options array instead.
* See the third example below for all options you can specify. (since 3.0.108)
*
* ~~~~~
* // Get string like "Items 1 to 25 of 500"
* echo $items->getPaginationString('Items');
*
* // Get string like "Page 1 of 10"
* echo $items->getPaginationString('Page', true);
*
* // Get string where you specify all options
* echo $items->getPaginationString(array(
* 'label' => 'Items',
* 'usePageNum' => false,
* 'count' => 10,
* 'start' => 0,
* 'limit' => 10,
* 'total' => 100
* ));
* ~~~~~
*
* #pw-group-other
*
* @param string $label Label to identify item type, i.e. "Items" or "Page", etc. (default=empty).
* @param bool $usePageNum Specify true to show page numbers rather than item numbers (default=false).
* Omit to use the default item numbers.
* @param string|array $label Label to identify item type, i.e. "Items" or "Page", etc. (default=empty).
* @param bool|array $usePageNum Specify true to show page numbers rather than item numbers (default=false).
* @return string Formatted string
*
*/
public function getPaginationString($label = '', $usePageNum = false) {
$count = $this->count();
$start = $this->getStart();
$limit = $this->getLimit();
$total = $this->getTotal();
$options = array(
'label' => is_string($label) ? $label : '',
'usePageNum' => is_bool($usePageNum) ? $usePageNum : false,
'count' => -1,
'start' => -1,
'limit' => -1,
'total' => -1
);
if(is_array($label)) $options = array_merge($options, $label);
if(is_array($usePageNum)) $options = array_merge($options, $usePageNum);
$label = $options['label'];
$usePageNum = $options['usePageNum'];
$count = $options['count'] > -1 ? $options['count'] : $this->count();
$start = $options['start'] > -1 ? $options['start'] : $this->getStart();
$limit = $options['limit'] > -1 ? $options['limit'] : $this->getLimit();
$total = $options['total'] > -1 ? $options['total'] : $this->getTotal();
if($usePageNum) {
$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

@@ -5,7 +5,7 @@
* Class to hold combined password/salt info. Uses Blowfish when possible.
* Specially used by FieldtypePassword.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
* @method setPass($value)
@@ -23,6 +23,12 @@ class Password extends Wire {
'hash' => '',
);
/**
* @var WireRandom|null
*
*/
protected $random = null;
/**
* Does this Password match the given string?
*
@@ -159,70 +165,18 @@ class Password extends Wire {
/**
* Generate a truly random base64 string of a certain length
*
* This is largely taken from Anthony Ferrara's password_compat library:
* https://github.com/ircmaxell/password_compat/blob/master/lib/password.php
* Modified for camelCase, variable names, and function-based context by Ryan.
* See WireRandom::base64() for details
*
* @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) {
$buffer = '';
$valid = false;
if($fast) {
// 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')) {
// @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(!$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;
}
}
}
if(!$valid || strlen($buffer) < $rawLength) {
$bl = strlen($buffer);
for($i = 0; $i < $rawLength; $i++) {
if($i < $bl) {
$buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
} else {
$buffer .= chr(mt_rand(0, 255));
}
}
}
$salt = str_replace('+', '.', base64_encode($buffer));
$salt = substr($salt, 0, $requiredLength);
//$salt .= $valid; // @todo: what was the point of this?j
return $salt;
public function randomBase64String($requiredLength = 22, $options = array()) {
return $this->random()->base64($requiredLength, $options);
}
/**
@@ -301,14 +255,19 @@ class Password extends Wire {
/**
* Return a pseudo-random alpha or alphanumeric character
*
*
* This method may be deprecated at some point, so it is preferable to use the
* `randomLetters()` or `randomAlnum()` methods instead, when you can count on
* the PW version being 3.0.109 or higher.
*
* @param int $qty Number of random characters requested
* @param bool $alphanumeric Specify true to allow digits in return value
* @param array $disallow Characters that may not be used in return value
* @return string
* @deprecated use WireRandom::alpha() instead
*
*/
protected function randomAlpha($qty = 1, $alphanumeric = false, $disallow = array()) {
public function randomAlpha($qty = 1, $alphanumeric = false, $disallow = array()) {
$letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$digits = '0123456789';
if($alphanumeric) $letters .= $digits;
@@ -324,168 +283,69 @@ class Password extends Wire {
return $value;
}
/**
* Return cryptographically secure random alphanumeric, alpha or numeric string
*
* @param int $length Required length of string, or 0 for random length
* @param array $options See WireRandom::alphanumeric() for options
* @return string
* @throws WireException
* @since 3.0.109
* @deprecated use WireRandom::alphanumeric() instead
*
*/
public function randomAlnum($length = 0, array $options = array()) {
return $this->random()->alphanumeric($length, $options);
}
/**
* Return string of random letters
*
* @param int $length Required length of string or 0 for random length
* @param array $options See options for randomAlnum() method
* @return string
* @since 3.0.109
* @deprecated use WireRandom::alpha() instead.
*
*/
public function randomLetters($length = 0, array $options = array()) {
return $this->random()->alpha($length, $options);
}
/**
* Return string of random digits
*
* @param int $length Required length of string or 0 for random length
* @param array $options See WireRandom::numeric() method
* @return string
* @since 3.0.109
* @deprecated Use WireRandom::numeric() instead
*
*/
public function randomDigits($length = 0, array $options = array()) {
return $this->random()->numeric($length, $options);
}
/**
* Generate and return a random password
*
* Default settings of this method are to generate a random but readable password without characters that
* tend to have readability issues, and using only ASCII characters (for broadest keyboard compatibility).
* See WireRandom::pass() method for details.
*
* @param array $options Specify any of the following options (all optional):
* - `minLength` (int): Minimum lenth of returned value (default=7).
* - `maxLength` (int): Maximum lenth of returned value, will be exceeded if needed to meet other options (default=15).
* - `minLower` (int): Minimum number of lowercase characters required (default=1).
* - `minUpper` (int): Minimum number of uppercase characters required (default=1).
* - `maxUpper` (int): Maximum number of uppercase characters allowed (0=any, -1=none, default=3).
* - `minDigits` (int): Minimum number of digits required (default=1).
* - `maxDigits` (int): Maximum number of digits allowed (0=any, -1=none, default=0).
* - `minSymbols` (int): Minimum number of non-alpha, non-digit symbols required (default=0).
* - `maxSymbols` (int): Maximum number of non-alpha, non-digit symbols to allow (0=any, -1=none, default=3).
* - `useSymbols` (array): Array of characters to use as "symbols" in returned value (see method for default).
* - `disallow` (array): Disallowed characters that may be confused with others (default=O,0,I,1,l).
*
* @param array $options See WireRandom::pass() for options
* @return string
*
*/
public function randomPass(array $options = array()) {
return $this->random()->pass($options);
}
$defaults = array(
'minLength' => 7,
'maxLength' => 15,
'minUpper' => 1,
'maxUpper' => 3,
'minLower' => 1,
'minDigits' => 1,
'maxDigits' => 0,
'minSymbols' => 0,
'maxSymbols' => 3,
'useSymbols' => array('@', '#', '$', '%', '^', '*', '_', '-', '+', '?', '(', ')', '!', '.', '=', '/'),
'disallow' => array('O', '0', 'I', '1', 'l'),
);
$options = array_merge($defaults, $options);
$length = mt_rand($options['minLength'], $options['maxLength']);
$base64Symbols = array('/' , '.');
$_disallow = array(); // with both upper and lower versions
foreach($options['disallow'] as $c) {
$c = strtolower($c);
$_disallow[$c] = $c;
$c = strtoupper($c);
$_disallow[$c] = $c;
}
// build foundation of password using base64 string
do {
$value = $this->randomBase64String($length);
$valid = preg_match('/[A-Z]/i', $value) && preg_match('/[0-9]/', $value);
} while(!$valid);
// limit amount of characters that are too common in base64 string
foreach($base64Symbols as $char) {
if(strpos($value, $char) === false) continue;
$c = $this->randomAlpha(1, true, $options['disallow']);
$value = str_replace($char, $c, $value);
}
// manage quantity of symbols
if($options['maxSymbols'] > -1) {
// ensure there are a certain quantity of symbols present
if($options['maxSymbols'] === 0) {
$numSymbols = mt_rand($options['minSymbols'], floor(strlen($value) / 2));
} else {
$numSymbols = mt_rand($options['minSymbols'], $options['maxSymbols']);
}
$symbols = $options['useSymbols'];
shuffle($symbols);
for($n = 0; $n < $numSymbols; $n++) {
$symbol = array_shift($symbols);
$value .= $symbol;
}
} else {
// no symbols, remove those commonly added in base64 string
$options['disallow'] = array_merge($options['disallow'], $base64Symbols);
}
// manage quantity of uppercase characters
if($options['maxUpper'] > 0 || ($options['minUpper'] > 0 && $options['maxUpper'] > -1)) {
// limit or establish the number of uppercase characters
if(!$options['maxUpper']) $options['maxUpper'] = floor(strlen($value) / 2);
$numUpper = mt_rand($options['minUpper'], $options['maxUpper']);
if($numUpper) {
$value = strtolower($value);
$test = $this->wire('sanitizer')->alpha($value);
if(strlen($test) < $numUpper) {
// there aren't enough characters present to meet requirements, so add some
$value .= $this->randomAlpha($numUpper - strlen($test), false, $_disallow);
}
for($i = 0; $i < strlen($value); $i++) {
$c = strtoupper($value[$i]);
if(in_array($c, $options['disallow'])) continue;
if($c !== $value[$i]) $value[$i] = $c;
if($c >= 'A' && $c <= 'Z') $numUpper--;
if(!$numUpper) break;
}
// still need more? append new characters as needed
if($numUpper) $value .= strtoupper($this->randomAlpha($numUpper, false, $_disallow));
}
} else if($options['maxUpper'] < 0) {
// disallow upper
$value = strtolower($value);
}
// manage quantity of lowercase characters
if($options['minLower'] > 0) {
$test = preg_replace('/[^a-z]/', '', $value);
if(strlen($test) < $options['minLower']) {
// needs more lowercase
$value .= strtolower($this->randomAlpha($options['minLower'] - strlen($test), false, $_disallow));
}
}
// manage quantity of required digits
if($options['minDigits'] > 0) {
$test = $this->wire('sanitizer')->digits($value);
$test = str_replace($options['disallow'], '', $test);
$numDigits = $options['minDigits'] - strlen($test);
if($numDigits > 0) {
$value .= $this->randomAlpha($numDigits, 1, $options['disallow']);
}
}
if($options['maxDigits'] > 0 || $options['maxDigits'] == -1) {
// a maximum number of digits specified
$numDigits = 0;
for($n = 0; $n < strlen($value); $n++) {
$c = $value[$n];
$isDigit = ctype_digit($c);
if($isDigit) $numDigits++;
if($isDigit && $numDigits > $options['maxDigits']) {
// convert digit to alpha
$value[$n] = strtolower($this->randomAlpha(1, false, $_disallow));
}
}
}
// replace any disallowed characters
foreach($options['disallow'] as $char) {
$pos = strpos($value, $char);
if($pos === false) continue;
if(ctype_digit($char)) {
$c = $this->randomAlpha(1, 1, $_disallow);
} else if(strtoupper($char) === $char) {
$c = strtoupper($this->randomAlpha(1, false, $_disallow));
} else {
$c = strtolower($this->randomAlpha(1, false, $_disallow));
}
$value = str_replace($char, $c, $value);
}
// randomize, in case any operations above need it
$value = str_split($value);
shuffle($value);
$value = implode('', $value);
return $value;
/**
* @return WireRandom
*
*/
protected function random() {
if($this->random === null) $this->random = $this->wire(new WireRandom());
return $this->random;
}
public function __toString() {

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,19 +126,21 @@ class Paths extends WireData {
$http = $_http;
$key = substr($key, 4);
$key[0] = strtolower($key[0]);
} else {
$http = '';
}
if($key == 'root') {
$value = $http . $this->_root;
} else {
$value = parent::get($key);
if(!is_null($value) && strlen($value)) {
if($value[0] == '/' || (DIRECTORY_SEPARATOR != '/' && $value[1] == ':')) {
$value = $http . $value;
} else {
$value = $http . $this->_root . $value;
}
if($value === null || !strlen($value)) return $value;
$pos = strpos($value, '//');
if($pos !== false && ($pos === 0 || ($pos > 0 && $value[$pos-1] === ':'))) {
// fully qualified URL
} else if($value[0] == '/' || (DIRECTORY_SEPARATOR != '/' && $value[1] == ':')) {
// path specifies its own root
$value = $http . $value;
} else {
// path needs root prepended
$value = $http . $this->_root . $value;
}
}
return $value;

View File

@@ -46,6 +46,25 @@ class Permissions extends PagesType {
'user-admin-all' => 'user-admin',
);
/**
* Permissions that can reduce existing access upon installation
*
* @var array
*
*/
protected $reducerPermissions = array(
'page-hide',
'page-publish',
'page-edit-created',
'page-edit-images',
'page-rename',
'page-edit-lang-',
'page-edit-lang-none',
'user-admin-',
'user-admin-all',
'page-lister-',
);
/**
* Does the system have a permission with the given name?
*
@@ -158,7 +177,9 @@ class Permissions extends PagesType {
$languages = $this->wire('languages');
if($languages) {
$label = $this->_('Edit fields on a page in language: %s');
$a["page-edit-lang-default"] = sprintf($label, 'default') . ' ' . $this->_('(also required to create or delete pages)');
$alsoLabel = $this->_('(also required to create or delete pages)');
$a["page-edit-lang-default"] = sprintf($label, 'default') . ' ' . $alsoLabel;
$a["page-edit-lang-none"] = $this->_('Edit single-language fields on multi-language page');
foreach($languages as $language) {
if($language->isDefault()) continue;
$a["page-edit-lang-$language->name"] = sprintf($label, $language->name);
@@ -180,6 +201,32 @@ class Permissions extends PagesType {
return $a;
}
/**
* Get permission names that can reduce existing access, when installed
*
* Returned permission names that end with a "-" indicate that given permission name is a prefix
* that applies for anything that appears after it.
*
* @return array Array of permission names where both index and value are permission name
*
*/
public function getReducerPermissions() {
$a = $this->reducerPermissions;
$languages = $this->wire('languages');
if($languages && $this->wire('modules')->isInstalled('LanguageSupportFields')) {
foreach($languages as $language) {
$a[] = "page-edit-lang-$language->name";
}
}
foreach($this->wire('roles') as $role) {
$a[] = "user-admin-$role->name";
}
$a = array_flip($a);
foreach($a as $k => $v) $a[$k] = $k;
return $a;
}
/**
* Return array of permission names that are delegated to another when not installed
*

View File

@@ -23,14 +23,15 @@
* @method Process headline(string $headline)
* @method Process browserTitle(string $title)
* @method Process breadcrumb(string $href, string $label)
* @method install()
* @method uninstall()
* @method upgrade($fromVersion, $toVersion)
* @method void install()
* @method void uninstall()
* @method void upgrade($fromVersion, $toVersion)
* @method Page installPage($name = '', $parent = null, $title = '', $template = 'admin', $extras = array()) #pw-internal
* @method int uninstallPage() #pw-internal
* @method string executeNavJSON(array $options = array()) #pw-internal @todo
* @method ready()
* @method setConfigData(array $data)
* @method void ready()
* @method void setConfigData(array $data)
* @method void executed($methodName) Hook called after a method has been executed in the Process
*
*/
@@ -125,7 +126,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 +136,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 +193,7 @@ abstract class Process extends WireData implements Module {
* ~~~~~
*
* @param string $headline
* @return this
* @return $this
*
*/
public function ___headline($headline) {
@@ -204,7 +209,7 @@ abstract class Process extends WireData implements Module {
* ~~~~~
*
* @param string $title
* @return this
* @return $this
*
*/
public function ___browserTitle($title) {
@@ -221,7 +226,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 +304,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 +498,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;
}
@@ -549,4 +559,19 @@ abstract class Process extends WireData implements Module {
public function getViewVars() {
return $this->_viewVars;
}
/**
* Return the Page that this process lives on
*
* @return Page|NullPage
*
*/
public function getProcessPage() {
$page = $this->wire('page');
if($page->process === $this) return $page;
$moduleID = $this->wire('modules')->getModuleID($this);
if(!$moduleID) return new NullPage();
$page = $this->wire('pages')->get("process=$moduleID, include=all");
return $page;
}
}

View File

@@ -5,23 +5,11 @@
*
* Loads and executes Process Module instance and determines access.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
*/
/**
* Exception thrown when a requested Process or Process method is requested that doesn't exist
*
*/
class ProcessController404Exception extends Wire404Exception { }
/**
* Exception thrown when the user doesn't have access to execute the requested Process
*
*/
class ProcessControllerPermissionException extends WirePermissionException { }
/**
* A Controller for Process* Modules
*
@@ -52,7 +40,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
@@ -60,7 +56,15 @@ class ProcessController extends Wire {
* @var string
*
*/
protected $processMethodName;
protected $processMethodName;
/**
* Process verbose module info
*
* @var array
*
*/
protected $processInfo = array();
/**
* The prefix to apply to the Process name
@@ -70,7 +74,7 @@ class ProcessController extends Wire {
* @var string
*
*/
protected $prefix;
protected $prefix;
/**
* Construct the ProcessController
@@ -141,21 +145,33 @@ 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 = '';
$info = $this->wire('modules')->getModuleInfo($processName, array('verbose' => false));
$info = $this->wire('modules')->getModuleInfoVerbose($processName);
$this->processInfo = $info;
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);
@@ -167,8 +183,6 @@ class ProcessController extends Wire {
*
* Note: an empty permission name is accessible only by the superuser
*
* @todo: This may now be completely unnecessary since permission checking is built into Modules.php
*
* @param string $permissionName
* @param bool $throw Whether to throw an Exception if the user does not have permission
* @return bool
@@ -179,47 +193,112 @@ class ProcessController extends Wire {
$user = $this->wire('user');
if($user->isSuperuser()) return true;
if($permissionName && $user->hasPermission($permissionName)) return true;
if($throw) throw new ProcessControllerPermissionException("You don't have $permissionName permission");
if($throw) {
throw new ProcessControllerPermissionException(
sprintf($this->_('You do not have “%s” permission'), $permissionName)
);
}
return false;
}
/**
* Does user have permission for the given $method name in the current Process?
*
* @param string $method
* @param bool $throw Throw exception if not permission?
* @return bool
* @throws ProcessControllerPermissionException
*
*/
protected function hasMethodPermission($method, $throw = true) {
// i.e. executeHelloWorld => helloWorld
$urlSegment = $method;
if(strpos($method, 'execute') === 0) list(,$urlSegment) = explode('execute', $method, 2);
$urlSegment = $this->wire('sanitizer')->hyphenCase($urlSegment);
if(!$this->hasUrlSegmentPermission($urlSegment, $throw)) return false;
return true;
}
/**
* Does user have permission for the given urlSegment in the current Process?
*
* @param string $urlSegment
* @param bool $throw Throw exception if not permission?
* @return bool
* @throws ProcessControllerPermissionException
*
*/
protected function hasUrlSegmentPermission($urlSegment, $throw = true) {
if(empty($this->processInfo['nav']) || $this->wire('user')->isSuperuser()) return true;
$hasPermission = true;
$urlSegment = trim(strtolower($urlSegment), '.-_');
foreach($this->processInfo['nav'] as $navItem) {
if(empty($navItem['permission'])) continue;
$navSegment = strtolower(trim($navItem['url'], './'));
if(empty($navSegment)) continue;
if(strpos($navSegment, '/') !== false) list($navSegment,) = explode($navSegment, '/', 2);
$navSegmentAlt = str_replace('-', '', $navSegment);
if($urlSegment === $navSegment || $urlSegment === $navSegmentAlt) {
$hasPermission = $this->hasPermission($navItem['permission'], $throw);
break;
}
}
return $hasPermission;
}
/**
* Get the name of the method to execute with the Process
*
* @param Process @process
* @return string
* @throws ProcessControllerPermissionException
*
*/
public function getProcessMethodName(Process $process) {
$method = $this->processMethodName;
$forceFail = false;
$urlSegment1 = $this->wire('input')->urlSegment1;
$method = self::defaultProcessMethodName;
$sanitizer = $this->wire('sanitizer');
if(!$method) {
$method = self::defaultProcessMethodName;
// urlSegment as given by ProcessPageView
$urlSegment1 = $this->input->urlSegment1;
if($urlSegment1 && !$this->user->isGuest()) {
if(strpos($urlSegment1, '-')) {
// urlSegment1 has multiple hyphenated parts: convert hello-world to HelloWorld
foreach(explode('-', $urlSegment1) as $v) $method .= ucfirst($v);
} else {
// just one part
$method .= ucfirst($urlSegment1);
}
if($this->processMethodName) {
// the method to use has been preset with the setProcessMethodName() function
$method = $this->processMethodName;
if($method !== self::defaultProcessMethodName) {
$this->hasMethodPermission($method);
}
} else if(strlen($urlSegment1) && !$this->wire('user')->isGuest()) {
// determine requested method from urlSegment1
// $urlSegment1 = trim($this->wire('sanitizer')->hyphenCase($urlSegment1, array('allow' => 'a-z0-9_')), '_');
if(ctype_alpha($urlSegment1)) {
$methodName = ucfirst($urlSegment1);
$hyphenName = $urlSegment1;
} else {
$methodName = trim($sanitizer->pascalCase($urlSegment1, array('allowUnderscore' => true)), '_');
$hyphenName = trim($sanitizer->hyphenCase($methodName, array('allowUnderscore' => true)), '_');
}
if($hyphenName != strtolower($urlSegment1) && strtolower($methodName) != strtolower($urlSegment1)) {
// if urlSegment changed from sanitization, likely not in valid format
$forceFail = true;
} else {
// valid
$method .= $methodName; // execute => executeHelloWorld
$this->hasUrlSegmentPermission($hyphenName);
}
}
if($forceFail) return '';
if($method === 'executed') return '';
$hookedMethod = "___$method";
if(method_exists($process, $method)
|| method_exists($process, $hookedMethod)
|| $process->hasHook($method . '()')) {
return $method;
} else {
return '';
}
if(method_exists($process, $method)) return $method;
if(method_exists($process, "___$method")) return $method;
if($process->hasHook($method . '()')) return $method;
return '';
}
/**
@@ -231,44 +310,53 @@ class ProcessController extends Wire {
*/
public function ___execute() {
$content = '';
$method = '';
$debug = $this->wire('config')->debug;
$breadcrumbs = $this->wire('breadcrumbs');
$headline = $this->wire('processHeadline');
$numBreadcrumbs = $breadcrumbs ? count($breadcrumbs) : null;
if($process = $this->getProcess()) {
if($method = $this->getProcessMethodName($this->process)) {
$className = $this->process->className();
if($debug) Debug::timer("$className.$method()");
$content = $this->process->$method();
if($debug) Debug::saveTimer("$className.$method()");
if($method != 'execute') {
// some method other than the main one
if(!is_null($numBreadcrumbs) && $numBreadcrumbs === count($breadcrumbs)) {
// process added no breadcrumbs, but there should be more
if($headline === $this->wire('processHeadline')) $process->headline(str_replace('execute', '', $method));
$moduleInfo = $this->wire('modules')->getModuleInfo($process);
$href = substr($this->wire('input')->url(), -1) == '/' ? '../' : './';
$process->breadcrumb($href, $moduleInfo['title']);
}
}
$this->process->executed($method);
} else {
throw new ProcessController404Exception("Unrecognized path");
}
$process = $this->getProcess();
if(!$process) {
throw new ProcessController404Exception("Process does not exist: $this->processError");
}
} else {
throw new ProcessController404Exception("The requested process does not exist");
// determine method (throws ProcessControllerPermissionException if no access)
$method = $this->getProcessMethodName($process);
if(!$method) {
throw new ProcessController404Exception("Unrecognized path");
}
if(empty($content) || is_bool($content)) {
$content = $this->process->getViewVars();
// call method from Process (and time it if debug mode enabled)
$className = $process->className();
if($debug) Debug::timer("$className.$method()");
$content = $process->$method();
if($debug) Debug::saveTimer("$className.$method()");
// setup breadcrumbs if in some method other than the main execute() method
if($method !== 'execute') {
// some method other than the main one
if(!is_null($numBreadcrumbs) && $numBreadcrumbs === count($breadcrumbs)) {
// process added no breadcrumbs, but there should be more
if($headline === $this->wire('processHeadline')) {
$process->headline(str_replace('execute', '', $method));
}
$href = substr($this->wire('input')->url(), -1) == '/' ? '../' : './';
$process->breadcrumb($href, $this->processInfo['title']);
}
}
// triggered "executed" (execute done) hook
$process->executed($method);
if(empty($content) || is_bool($content)) {
$content = $process->getViewVars();
}
if(is_array($content)) {
// array of returned content indicates variables to send to a view
if(count($content)) {
$viewFile = $this->getViewFile($this->process, $method);
if(count($content) || $process->getViewFile()) {
$viewFile = $this->getViewFile($process, $method);
if($viewFile) {
// get output from a separate view file
$template = $this->wire(new TemplateFile($viewFile));

View File

@@ -17,7 +17,7 @@ require_once(__DIR__ . '/boot.php');
* ~~~~~
* #pw-body
*
* ProcessWire 3.x, Copyright 2017 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
* @method init()
@@ -26,7 +26,6 @@ require_once(__DIR__ . '/boot.php');
*
*
*/
class ProcessWire extends Wire {
/**
@@ -45,7 +44,7 @@ class ProcessWire extends Wire {
* Reversion revision number
*
*/
const versionRevision = 62;
const versionRevision = 123;
/**
* Version suffix string (when applicable)
@@ -204,11 +203,12 @@ class ProcessWire extends Wire {
$this->wire('hooks', new WireHooks($this, $config), true);
$this->shutdown = $this->wire(new WireShutdown());
$this->setConfig($config);
$this->shutdown = $this->wire(new WireShutdown($config));
$this->setStatus(self::statusBoot);
$this->load($config);
if($this->getNumInstances() > 1) {
if(self::getNumInstances() > 1) {
// this instance is not handling the request and needs a mock $page API var and pageview
/** @var ProcessPageView $view */
$view = $this->wire('modules')->get('ProcessPageView');
@@ -220,7 +220,7 @@ class ProcessWire extends Wire {
$str = $this->className() . " ";
$str .= self::versionMajor . "." . self::versionMinor . "." . self::versionRevision;
if(self::versionSuffix) $str .= " " . self::versionSuffix;
if($this->getNumInstances() > 1) $str .= " #$this->instanceID";
if(self::getNumInstances() > 1) $str .= " #$this->instanceID";
return $str;
}
@@ -274,22 +274,27 @@ class ProcessWire extends Wire {
$config->debug = $debugIf;
}
// If script is being called externally, add an extra shutdown function
if(!$config->internal) register_shutdown_function(function() {
if(error_get_last()) return;
$process = isset($this) ? $this->wire('process') : wire('process');
if($process == 'ProcessPageView') $process->finished();
});
if($config->useFunctionsAPI) {
$file = $config->paths->core . 'FunctionsAPI.php';
/** @noinspection PhpIncludeInspection */
include_once($file);
}
$this->setStatus(self::statusBoot);
// check if noHTTPS option is using conditional hostname
if($config->noHTTPS && $config->noHTTPS !== true) {
$noHTTPS = $config->noHTTPS;
$httpHost = $config->httpHost;
if(is_string($noHTTPS)) $noHTTPS = array($noHTTPS);
if(is_array($noHTTPS)) {
$config->noHTTPS = false;
foreach($noHTTPS as $host) {
if($host === $httpHost) {
$config->noHTTPS = true;
break;
}
}
}
}
}
/**
@@ -432,7 +437,8 @@ class ProcessWire extends Wire {
$session = $this->wire('session', new Session($this), true);
$this->initVar('session', $session);
$this->wire('user', $users->getCurrentUser());
$this->wire('input', new WireInput(), true);
$input = $this->wire('input', new WireInput(), true);
if($config->wireInputLazy) $input->setLazy(true);
// populate admin URL before modules init()
$config->urls->admin = $config->urls->root . ltrim($pages->getPath($config->adminRootPageID), '/');
@@ -734,6 +740,41 @@ class ProcessWire extends Wire {
}
}
/**
* Get root path, check it, and optionally auto-detect it if not provided
*
* @param bool|string $rootPath Root path if already known, in which case well just modify as needed
* @return string
*
*/
protected static function getRootPath($rootPath = '') {
if(strpos($rootPath, '..') !== false) {
$rootPath = realpath($rootPath);
}
if(empty($rootPath) && !empty($_SERVER['SCRIPT_FILENAME'])) {
// first try to determine from the script filename
$parts = explode(DIRECTORY_SEPARATOR, $_SERVER['SCRIPT_FILENAME']);
array_pop($parts); // most likely: index.php
$rootPath = implode('/', $parts) . '/';
if(!file_exists($rootPath . 'wire/core/ProcessWire.php')) $rootPath = '';
}
if(empty($rootPath)) {
// if unable to determine from script filename, attempt to determine from current file
$parts = explode(DIRECTORY_SEPARATOR, __FILE__);
$parts = array_slice($parts, 0, -3); // removes "ProcessWire.php", "core" and "wire"
$rootPath = implode('/', $parts) . '/';
}
if(DIRECTORY_SEPARATOR != '/') {
$rootPath = str_replace(DIRECTORY_SEPARATOR, '/', $rootPath);
}
return $rootPath;
}
/**
* Static method to build a Config object for booting ProcessWire
*
@@ -745,14 +786,9 @@ class ProcessWire extends Wire {
* @return Config
*
*/
public static function buildConfig($rootPath, $rootURL = null, array $options = array()) {
if(DIRECTORY_SEPARATOR != '/') {
$rootPath = str_replace(DIRECTORY_SEPARATOR, '/', $rootPath);
}
if(strpos($rootPath, '..') !== false) $rootPath = realpath($rootPath);
public static function buildConfig($rootPath = '', $rootURL = null, array $options = array()) {
$rootPath = self::getRootPath($rootPath);
$httpHost = '';
$scheme = '';
$siteDir = isset($options['siteDir']) ? $options['siteDir'] : 'site';
@@ -781,7 +817,7 @@ class ProcessWire extends Wire {
$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
}
}
@@ -804,7 +840,7 @@ class ProcessWire extends Wire {
unset($sf, $f, $x);
// when internal is true, we are not being called by an external script
$cfg['internal'] = $realIndexFile == $realScriptFile;
$cfg['internal'] = strtolower($realIndexFile) == strtolower($realScriptFile);
} else {
// when included from another app or command line script

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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -108,7 +108,7 @@ abstract class Selector extends WireData {
*
*/
public function operator() {
return self::getOperator();
return static::getOperator();
}
/**
@@ -181,20 +181,38 @@ abstract class Selector extends WireData {
/**
* 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() {
public function values($nonEmpty = false) {
$values = parent::get('value');
if(is_array($values)) return $values;
if(!is_object($values) && !strlen($values)) return array();
return array($values);
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') return $this->values();
if($key == 'fields') return $this->fields();
@@ -259,7 +277,7 @@ abstract class Selector extends WireData {
*
* @param string $key
* @param mixed $value
* @return $this
* @return Selector|WireData
*
*/
public function set($key, $value) {
@@ -302,7 +320,7 @@ abstract class Selector extends WireData {
*
* If the value held by this Selector is an array of values, it will check if any one of them matches the value supplied here.
*
* @param string|int|Wire $value If given a Wire, then matches will also operate on OR field=value type selectors, where present
* @param string|int|Wire|array $value If given a Wire, then matches will also operate on OR field=value type selectors, where present
* @return bool
*
*/
@@ -314,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)) {
@@ -402,7 +420,7 @@ 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;
}

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