mirror of
https://github.com/processwire/processwire.git
synced 2025-08-28 08:59:52 +02:00
Compare commits
403 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
8ee607f06f | ||
|
0b4262b57b | ||
|
4cb2061925 | ||
|
bf22960d11 | ||
|
d6837c8e5c | ||
|
c923a1b568 | ||
|
03e7b13fce | ||
|
db0d1321c8 | ||
|
e95661c439 | ||
|
dc89e16131 | ||
|
c1fb7264c8 | ||
|
4acfc353cd | ||
|
abab9bf4a8 | ||
|
8b39c92b31 | ||
|
7d5ad4b277 | ||
|
27f20b4799 | ||
|
ff69aa56a1 | ||
|
eda5129884 | ||
|
5fb798857f | ||
|
ece4917b1b | ||
|
e6160973e0 | ||
|
0da85dd55c | ||
|
95cca5ad62 | ||
|
a817943b73 | ||
|
96165bbff4 | ||
|
f03498e88e | ||
|
d11a1e631b | ||
|
3770a8f74b | ||
|
f4487628ad | ||
|
f15c5942b4 | ||
|
e526d6562d | ||
|
b579e24425 | ||
|
1b0d51e275 | ||
|
36980a2455 | ||
|
5373613169 | ||
|
c9650f1974 | ||
|
9f9cc65680 | ||
|
99cdf49bad | ||
|
24ac500048 | ||
|
eb48adccb2 | ||
|
c1259148d2 | ||
|
6566c5795c | ||
|
44e6c89e35 | ||
|
050fb0aa0c | ||
|
b066de77b6 | ||
|
731578fbed | ||
|
8248143aee | ||
|
9fb83ffe66 | ||
|
f627e68a6f | ||
|
6a48a29b2d | ||
|
bc0faa771f | ||
|
b786dcff14 | ||
|
e0f96733e6 | ||
|
77fbde3409 | ||
|
fb26ec9d64 | ||
|
e8f8317b48 | ||
|
8322725f35 | ||
|
c1d150d4ba | ||
|
b5f94df3d6 | ||
|
a3dbd1b28f | ||
|
69823256fb | ||
|
436a0ed3b1 | ||
|
5f17f5ff9d | ||
|
0a317971f8 | ||
|
b4ddc89fb9 | ||
|
570bdb4b81 | ||
|
725d89e664 | ||
|
2715611949 | ||
|
ee96262e44 | ||
|
dae4e59db9 | ||
|
bc8a1959f3 | ||
|
6783c4824b | ||
|
4725ece5f8 | ||
|
422d9c1eb6 | ||
|
a17ebff0cd | ||
|
afacf91792 | ||
|
0c3c15c29d | ||
|
b7636bc1df | ||
|
a8ea954462 | ||
|
8ee238ea70 | ||
|
668081fb23 | ||
|
9996091afb | ||
|
44fcf13ea2 | ||
|
69270a31b0 | ||
|
f88350baa5 | ||
|
c7ba08ecb9 | ||
|
59ae7f4c7f | ||
|
0708865081 | ||
|
6ae349b4ec | ||
|
30b34c70b3 | ||
|
e6dbc3e8eb | ||
|
a959afc422 | ||
|
4a9b904b77 | ||
|
8b5d96f1b6 | ||
|
eddd6cb8ad | ||
|
e1e938591d | ||
|
1805ad0a59 | ||
|
f22739a54c | ||
|
4e678c1584 | ||
|
4604c09abc | ||
|
be3d17b9c2 | ||
|
54e75701c1 | ||
|
29b1fa0e45 | ||
|
870284072c | ||
|
552fd7180e | ||
|
9db14e6aef | ||
|
8d2ad63ce7 | ||
|
f1819b5cd8 | ||
|
807e94e22a | ||
|
bd5200dfb2 | ||
|
00a6baaac9 | ||
|
e6ace73c02 | ||
|
4be389067d | ||
|
fa47338eed | ||
|
cef47391ee | ||
|
86fc754ffb | ||
|
6036118b15 | ||
|
16d70048c1 | ||
|
1f7d039b3e | ||
|
94bc7c346e | ||
|
b7238605e4 | ||
|
1fc3cf414a | ||
|
9bc02399e5 | ||
|
68fa2b47f6 | ||
|
2361b90739 | ||
|
1c5f2f7e3c | ||
|
ac4dfebfab | ||
|
405da182d5 | ||
|
0ea71c3e1d | ||
|
ede080e2a8 | ||
|
496509c39f | ||
|
7b893abba3 | ||
|
cf0abe538a | ||
|
3bd27723b2 | ||
|
1a5760a5e8 | ||
|
5ca977f6df | ||
|
57388db576 | ||
|
fb641fae89 | ||
|
a14398b4a3 | ||
|
8a1ba87298 | ||
|
53b7aa39eb | ||
|
ca74514288 | ||
|
06ac399319 | ||
|
b374ed83e2 | ||
|
6c8ca289ba | ||
|
ec8943c26d | ||
|
5a8732f1e1 | ||
|
1191c164a2 | ||
|
d84d40e84c | ||
|
5481d713ab | ||
|
af5cbd7e3c | ||
|
4d6589bdc8 | ||
|
b2b810f181 | ||
|
5e91b745e1 | ||
|
6cd8516a6f | ||
|
9dbd7dd079 | ||
|
0ef8a4de0b | ||
|
1c0aa2d248 | ||
|
4f7161fd49 | ||
|
5abf2077c7 | ||
|
6d479ba52c | ||
|
fef2a76f39 | ||
|
965f956bc3 | ||
|
754b1fffb7 | ||
|
fae4fac013 | ||
|
c4257ee646 | ||
|
d9399bc673 | ||
|
6ff9109583 | ||
|
ffddd85566 | ||
|
67da683ff6 | ||
|
fcc0e72868 | ||
|
842eca45b9 | ||
|
80f425f9da | ||
|
e51ece23fe | ||
|
95bd1d426c | ||
|
9e8ffac63f | ||
|
f77dd242dc | ||
|
ce01e699e3 | ||
|
1fdc61dc41 | ||
|
6e93844c19 | ||
|
2a84f12018 | ||
|
137b2aa50b | ||
|
19fb83201d | ||
|
bda807a574 | ||
|
5b0e37e3ae | ||
|
962d26a749 | ||
|
e508cfa2a7 | ||
|
4ee947d237 | ||
|
acc7ca2d91 | ||
|
e08fa2e957 | ||
|
18084dd8ef | ||
|
899ffd186a | ||
|
36227dc778 | ||
|
2690115966 | ||
|
98968d796f | ||
|
dff3e8aaeb | ||
|
d5faf861dc | ||
|
0500293f96 | ||
|
7a43790412 | ||
|
b29e6a45c0 | ||
|
d48588f508 | ||
|
1222a1598b | ||
|
5609fde13a | ||
|
92afe679b9 | ||
|
061170204b | ||
|
6d225f3c99 | ||
|
38a5320f61 | ||
|
abe1216c89 | ||
|
cf0832c330 | ||
|
34c10a5417 | ||
|
cc79223bc8 | ||
|
13221c3bd5 | ||
|
e78ada8854 | ||
|
48f85faced | ||
|
f6a1ea781b | ||
|
7988319c72 | ||
|
7c85b089dd | ||
|
34bca47a07 | ||
|
b9d8a741ee | ||
|
d50cc127cc | ||
|
904c227cce | ||
|
00ae62059b | ||
|
9803df9401 | ||
|
3c5205721b | ||
|
049efa7c3b | ||
|
212d2b361b | ||
|
7c89b2b647 | ||
|
9eb58ead01 | ||
|
faf27c8fa1 | ||
|
764153732e | ||
|
172ad1c812 | ||
|
eaed402cfb | ||
|
397bb0b382 | ||
|
d77b23adbb | ||
|
4e2ef8f8fd | ||
|
bbaa5570fb | ||
|
dcd820064b | ||
|
bae44f93ce | ||
|
c38c204824 | ||
|
38eadb46d8 | ||
|
4e2d798d49 | ||
|
a37f237900 | ||
|
29ecddadeb | ||
|
57b23ef9fe | ||
|
3c0e9f3c43 | ||
|
def74f7b6d | ||
|
7a85039896 | ||
|
432e369990 | ||
|
9a6963a644 | ||
|
76388b48e6 | ||
|
d8ae8f9177 | ||
|
9e6b89cf93 | ||
|
7438ae90ca | ||
|
6aa698343b | ||
|
9eb9f88090 | ||
|
9737b4e15d | ||
|
37416f8bcc | ||
|
e0f67aa55e | ||
|
91c15f666a | ||
|
ffdd9729e4 | ||
|
718c93b056 | ||
|
68d9ec9b42 | ||
|
fb12fb7750 | ||
|
6e1d7b166d | ||
|
21949387b4 | ||
|
0852242866 | ||
|
4f55480fc7 | ||
|
3256cb9000 | ||
|
55a241e2f1 | ||
|
5b257c6031 | ||
|
38757b1baa | ||
|
d7502b669a | ||
|
5fe181c315 | ||
|
4b55979624 | ||
|
4099035708 | ||
|
9770138eee | ||
|
04041bb54a | ||
|
128538fcd8 | ||
|
76ad3ab984 | ||
|
f893cec515 | ||
|
837a8fd32a | ||
|
37ef2c9070 | ||
|
9c14e27576 | ||
|
d5116166d0 | ||
|
47c639617c | ||
|
1f2d597f52 | ||
|
c6dc986f9c | ||
|
b5d8a91e49 | ||
|
f3e614640b | ||
|
3e90cb74fa | ||
|
71a1e9c9d9 | ||
|
a53b4e5310 | ||
|
3ab315dca4 | ||
|
94653012be | ||
|
f801fef42b | ||
|
caa8e7e421 | ||
|
a5f6cabbcf | ||
|
db358ee4db | ||
|
215010386f | ||
|
97db8e8783 | ||
|
9fe7e95840 | ||
|
d2fccd84af | ||
|
660ea79496 | ||
|
0a926b58fa | ||
|
a3f884146f | ||
|
8c80c524b1 | ||
|
f02393e538 | ||
|
9af0aaf2b2 | ||
|
ddbbbcc4e6 | ||
|
ef3ee4645f | ||
|
b41c0dd098 | ||
|
95e10b89b9 | ||
|
d37b2d40d7 | ||
|
db04f2d2e6 | ||
|
091d875f50 | ||
|
bc888f8b52 | ||
|
91eff3074d | ||
|
511f237429 | ||
|
a3aa5c4dd0 | ||
|
3856a200ea | ||
|
1647690bc8 | ||
|
50a7b4c7c4 | ||
|
1216340a46 | ||
|
3717a85f3b | ||
|
bad69efd8e | ||
|
275651bb5a | ||
|
52da051446 | ||
|
9dec9782e1 | ||
|
32d425aead | ||
|
baf05a8777 | ||
|
8a1f706be9 | ||
|
98fe7f94a0 | ||
|
ef4444dd7f | ||
|
409c0c0a68 | ||
|
2cc3960c68 | ||
|
dd146a4be8 | ||
|
50cf963c88 | ||
|
b61da8575a | ||
|
ee217ee3bd | ||
|
3220b7dc40 | ||
|
86c0af08d0 | ||
|
a02020cef0 | ||
|
c205d475bf | ||
|
dcb1f47ae3 | ||
|
2e62550133 | ||
|
dabc56043f | ||
|
7e7a760b88 | ||
|
99a1d0f81d | ||
|
019d5c6014 | ||
|
b3d84f15e1 | ||
|
8c11a9939c | ||
|
d82194816f | ||
|
c62deb7946 | ||
|
100711c2f4 | ||
|
7a512a15a6 | ||
|
92ea8eb074 | ||
|
3e323e5f2f | ||
|
993b5cc162 | ||
|
4aa7104378 | ||
|
adac6a1e30 | ||
|
8343fd2365 | ||
|
c86d39f146 | ||
|
c2d7b47e12 | ||
|
ae3287ed33 | ||
|
260e0f228e | ||
|
c1df78b0a6 | ||
|
4ffde04a5c | ||
|
3cbae7da97 | ||
|
e172dd011b | ||
|
233a66f846 | ||
|
3a6e8ffcda | ||
|
e53552d8c4 | ||
|
78aea1eedf | ||
|
8974100c42 | ||
|
db2112defd | ||
|
463dd01e66 | ||
|
88ad063af1 | ||
|
996a1b6854 | ||
|
ee6f88dec2 | ||
|
1f4d32ded9 | ||
|
8571be1b23 | ||
|
6d2c8bf795 | ||
|
173f1b1b29 | ||
|
3cc76cc886 | ||
|
3ff60a289c | ||
|
390ad61ce3 | ||
|
4355654d16 | ||
|
d68c782c8d | ||
|
96c7ecfb34 | ||
|
3ba7e2f483 | ||
|
ec2777432d | ||
|
a1ebb5d0df | ||
|
5609935e4e | ||
|
17e07e7859 | ||
|
a0fabd6811 | ||
|
21e370b7ee | ||
|
17d6def379 | ||
|
c6844af963 | ||
|
6050a7139c | ||
|
cb5579a8c9 | ||
|
dd8f2a5c63 | ||
|
b0414278f8 | ||
|
36580883d7 |
18
README.md
18
README.md
@@ -40,7 +40,7 @@ development branch.
|
||||
ProcessWire is a timeless tool for web professionals that has always been
|
||||
committed to the long term. It started in 2003, gained the name ProcessWire
|
||||
in 2006, and has been in active development as an open source project since 2010.
|
||||
Now more than a decade later (2023), we’re just getting started, as ProcessWire
|
||||
Now more than a decade later (2025), we’re just getting started, as ProcessWire
|
||||
continues to grow and develop into the next 10 years and beyond.
|
||||
|
||||
While ProcessWire has been around for a long time, don’t feel bad if you haven’t
|
||||
@@ -71,9 +71,7 @@ in the ProcessWire forums, subscribe to our
|
||||
[weekly newsletter](https://processwire.com/community/newsletter/subscribe/)
|
||||
for the latest ProcessWire news, check out our
|
||||
[website showcase](https://processwire.com/sites/)
|
||||
to see what others are building with ProcessWire, and read our
|
||||
[blog](https://processwire.com/blog/)
|
||||
to stay up-to-date with the latest ProcessWire versions.
|
||||
to see what others are building with ProcessWire.
|
||||
|
||||
Weekly ProcessWire news is posted by Teppo Koivula on his site
|
||||
[ProcessWire Weekly](https://weekly.pw).
|
||||
@@ -129,15 +127,15 @@ replacing your `/wire/` directory with the one from the newer version.
|
||||
### Pro module version upgrade notes (if applicable)
|
||||
|
||||
- [FormBuilder](https://processwire.com/store/form-builder/)
|
||||
version 0.5.3 or newer recommended.
|
||||
version 0.5.5 or newer recommended.
|
||||
- [ListerPro](https://processwire.com/store/lister-pro/)
|
||||
version 1.1.5 or newer recommended.
|
||||
version 1.1.6 or newer recommended.
|
||||
- [ProFields](https://processwire.com/store/pro-fields/)
|
||||
the latest versions of all ProFields (10 modules) are recommended.
|
||||
- [LoginRegisterPro](https://processwire.com/store/login-register-pro/)
|
||||
version 7 or newer recommended.
|
||||
version 8 or newer recommended.
|
||||
- [ProCache](https://processwire.com/store/pro-cache/)
|
||||
version 4.0.3 or newer recommended. After upgrading, go to your ProCache
|
||||
version 4.0.5 or newer recommended. After upgrading, go to your ProCache
|
||||
settings in the admin (Setup > ProCache) and see if it suggests any
|
||||
modifications to your .htaccess file.
|
||||
|
||||
@@ -171,14 +169,12 @@ resolved any issues.
|
||||
|
||||
* [ProcessWire Support Forums](https://processwire.com/talk/)
|
||||
* [ProcessWire Weekly News](https://weekly.pw/)
|
||||
* [ProcessWire Blog](https://processwire.com/blog/)
|
||||
* [Sites running ProcessWire](https://processwire.com/sites/)
|
||||
* [Subscribe to ProcessWire Weekly email](https://processwire.com/community/newsletter/subscribe/)
|
||||
* [Submit your site to our directory](https://processwire.com/sites/submit/)
|
||||
* [Follow @processwire on X-Twitter](http://twitter.com/processwire/)
|
||||
* [Contact ProcessWire developer](https://processwire.com/contact/)
|
||||
* [Report issue](https://github.com/processwire/processwire-issues/issues)
|
||||
|
||||
------
|
||||
|
||||
Copyright 2023 by Ryan Cramer / Ryan Cramer Design, LLC
|
||||
Copyright 2025 by Ryan Cramer / Ryan Cramer Design, LLC
|
@@ -14,7 +14,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.5",
|
||||
"php": ">=7.1",
|
||||
"ext-gd": "*"
|
||||
},
|
||||
"autoload": {
|
||||
|
@@ -390,7 +390,7 @@ DirectoryIndex index.php index.html index.htm
|
||||
RewriteCond %{REQUEST_URI} (^|/)(site|site-[^/]+)/modules/.*\.(php|inc|tpl|module|info\.json)$ [NC,OR]
|
||||
|
||||
# Block access to any software identifying txt, markdown or textile files
|
||||
RewriteCond %{REQUEST_URI} (^|/)(COPYRIGHT|INSTALL|README|htaccess)\.(txt|md|textile)$ [NC,OR]
|
||||
RewriteCond %{REQUEST_URI} (^|/)(COPYRIGHT|INSTALL|README|CHANGELOG|LICENSE|htaccess)\.(txt|md|textile)$ [NC,OR]
|
||||
|
||||
# Block potential arbitrary backup files within site directories for things like config
|
||||
RewriteCond %{REQUEST_URI} (^|/)(site|site-[^/]+)/(config[^/]*/?|[^/]+\.php.*)$ [NC,OR]
|
||||
|
183
install.php
183
install.php
@@ -11,7 +11,7 @@
|
||||
* If that file exists, the installer will not run. So if you need to re-run this installer for any
|
||||
* reason, then you'll want to delete that file. This was implemented just in case someone doesn't delete the installer.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @todo 3.0.190: provide option for command-line options to install
|
||||
@@ -47,7 +47,7 @@ class Installer {
|
||||
* Minimum required PHP version to install ProcessWire
|
||||
*
|
||||
*/
|
||||
const MIN_REQUIRED_PHP_VERSION = '5.3.8';
|
||||
const MIN_REQUIRED_PHP_VERSION = '7.1.0';
|
||||
|
||||
/**
|
||||
* Test mode for installer development, non destructive
|
||||
@@ -91,18 +91,19 @@ class Installer {
|
||||
public function execute() {
|
||||
|
||||
if(self::TEST_MODE) {
|
||||
error_reporting(E_ALL | E_STRICT);
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
}
|
||||
|
||||
// these two vars used by install-head.inc
|
||||
$title = "ProcessWire " . PROCESSWIRE_INSTALL . " Installer";
|
||||
$formAction = "./install.php";
|
||||
|
||||
require("./wire/modules/AdminTheme/AdminThemeUikit/install-head.inc");
|
||||
|
||||
$step = $this->post('step');
|
||||
|
||||
if($step === '5') require('./index.php');
|
||||
|
||||
require("./wire/modules/AdminTheme/AdminThemeUikit/install-head.inc");
|
||||
|
||||
if($step === null) {
|
||||
$this->welcome();
|
||||
} else {
|
||||
@@ -112,7 +113,7 @@ class Installer {
|
||||
case 1: $this->compatibilityCheck(); break;
|
||||
case 2: $this->dbConfig(); break;
|
||||
case 4: $this->dbSaveConfig(); break;
|
||||
case 5: require("./index.php");
|
||||
case 5:
|
||||
/** @var ProcessWire $wire */
|
||||
$wire->modules->refresh();
|
||||
$this->adminAccountSave($wire);
|
||||
@@ -187,7 +188,7 @@ class Installer {
|
||||
if($dir->isDot() || !$dir->isDir()) continue;
|
||||
$name = $dir->getBasename();
|
||||
$path = rtrim($dir->getPathname(), '/') . '/';
|
||||
if(strpos($name, 'site-') !== 0) continue;
|
||||
if(strpos($name, 'site-') !== 0 && $name !== 'site') continue;
|
||||
$passed = true;
|
||||
foreach($dirTests as $test) if(!is_dir($path . $test)) $passed = false;
|
||||
foreach($fileTests as $test) if(!file_exists($path . $test)) $passed = false;
|
||||
@@ -402,11 +403,15 @@ class Installer {
|
||||
$this->warn("Consider making directory $d writable, at least during development.");
|
||||
}
|
||||
}
|
||||
|
||||
if(is_writable("./site/config.php")) {
|
||||
$this->ok("/site/config.php is writable");
|
||||
|
||||
if(file_exists("./site/config.php")) {
|
||||
if(is_writable("./site/config.php")) {
|
||||
$this->ok("/site/config.php is writable");
|
||||
} else {
|
||||
$this->err("/site/config.php must be writable during installation. Please adjust the server permissions before continuing.");
|
||||
}
|
||||
} else {
|
||||
$this->err("/site/config.php must be writable. Please adjust the server permissions before continuing.");
|
||||
$this->err("Site profile is missing a /site/config.php file.");
|
||||
}
|
||||
|
||||
if(!is_file("./.htaccess") || !is_readable("./.htaccess")) {
|
||||
@@ -474,11 +479,13 @@ class Installer {
|
||||
if(!isset($values['dbPort'])) $values['dbPort'] = ini_get("mysqli.default_port");
|
||||
if(!isset($values['dbUser'])) $values['dbUser'] = ini_get("mysqli.default_user");
|
||||
if(!isset($values['dbPass'])) $values['dbPass'] = ini_get("mysqli.default_pw");
|
||||
if(!isset($values['dbEngine'])) $values['dbEngine'] = 'MyISAM';
|
||||
if(!isset($values['dbEngine'])) $values['dbEngine'] = 'InnoDB';
|
||||
if(!isset($values['dbSocket'])) $values['dbSocket'] = ini_get("mysqli.default_socket");
|
||||
if(!isset($values['dbCon'])) $values['dbCon'] = 'Hostname';
|
||||
|
||||
if(!$values['dbHost']) $values['dbHost'] = 'localhost';
|
||||
if(!$values['dbPort']) $values['dbPort'] = 3306;
|
||||
if(empty($values['dbCharset'])) $values['dbCharset'] = 'utf8';
|
||||
if(empty($values['dbCharset'])) $values['dbCharset'] = 'utf8mb4';
|
||||
if($values['dbCharset'] != 'utf8mb4') $values['dbCharset'] = 'utf8';
|
||||
if($values['dbEngine'] != 'InnoDB') $values['dbEngine'] = 'MyISAM';
|
||||
|
||||
@@ -486,22 +493,45 @@ class Installer {
|
||||
if(strpos($key, 'chmod') === 0) {
|
||||
$values[$key] = (int) $value;
|
||||
} else if($key != 'httpHosts') {
|
||||
$values[$key] = htmlspecialchars($value, ENT_QUOTES, 'utf-8');
|
||||
$values[$key] = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
$this->input('dbName', 'DB Name', $values['dbName']);
|
||||
$this->input('dbUser', 'DB User', $values['dbUser']);
|
||||
$this->input('dbPass', 'DB Pass', $values['dbPass'], array('type' => 'password', 'required' => false));
|
||||
$this->input('dbHost', 'DB Host', $values['dbHost']);
|
||||
$this->input('dbPass', 'DB Pass', $values['dbPass'], array('type' => 'password', 'required' => false));
|
||||
$this->select('dbCon', 'Connection', $values['dbCon'], array('Hostname', 'Socket'));
|
||||
$this->clear();
|
||||
|
||||
$this->input('dbHost', 'DB Host', $values['dbHost']);
|
||||
$this->input('dbPort', 'DB Port', $values['dbPort']);
|
||||
|
||||
$this->select('dbCharset', 'DB Charset', $values['dbCharset'], array('utf8', 'utf8mb4'));
|
||||
$this->select('dbEngine', 'DB Engine', $values['dbEngine'], array('MyISAM', 'InnoDB'));
|
||||
$this->input('dbSocket', 'DB Socket', $values['dbSocket'], array('width' => 300));
|
||||
$this->select('dbCharset', 'DB Charset', $values['dbCharset'], array('utf8mb4', 'utf8'));
|
||||
$this->select('dbEngine', 'DB Engine', $values['dbEngine'], array('InnoDB', 'MyISAM'));
|
||||
$this->clear();
|
||||
|
||||
// automatic required states for host, port and socket
|
||||
echo "
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
let ho = $('input[name=dbHost]'), po = $('input[name=dbPort]'),
|
||||
so = $('input[name=dbSocket]'), co = $('select[name=dbCon]');
|
||||
co.on('change', function() {
|
||||
if(co.val() === 'Hostname') {
|
||||
ho.prop('required', true).closest('p').show();
|
||||
po.prop('required', true).closest('p').show();
|
||||
so.prop('required', false).closest('p').hide();
|
||||
} else {
|
||||
ho.prop('required', false).closest('p').hide();
|
||||
po.prop('required', false).closest('p').hide();
|
||||
so.prop('required', true).closest('p').show();
|
||||
}
|
||||
}).change();
|
||||
});
|
||||
</script>
|
||||
";
|
||||
|
||||
$this->p(
|
||||
"The DB Charset option “utf8mb4” may not be compatible with all 3rd party modules.<br />" .
|
||||
"The DB Engine option “InnoDB” requires MySQL 5.6.4 or newer.",
|
||||
array('class' => 'detail', 'style' => 'margin-top:0')
|
||||
);
|
||||
@@ -666,27 +696,37 @@ class Installer {
|
||||
$values['debugMode'] = $this->post('debugMode', 'int');
|
||||
|
||||
// db configuration
|
||||
$fields = array('dbUser', 'dbName', 'dbPass', 'dbHost', 'dbPort', 'dbEngine', 'dbCharset');
|
||||
$fields = array('dbUser', 'dbName', 'dbPass', 'dbHost', 'dbPort', 'dbSocket', 'dbEngine', 'dbCharset', 'dbCon');
|
||||
|
||||
foreach($fields as $field) {
|
||||
$value = $this->post($field, 'string');
|
||||
$value = substr($value, 0, 255);
|
||||
if(strpos($value, "'") !== false) $value = str_replace("'", "\\" . "'", $value); // allow for single quotes (i.e. dbPass)
|
||||
if($field != 'dbPass') $value = str_replace(array(';', '..', '=', '<', '>', '&', '"', "\t", "\n", "\r"), '', $value);
|
||||
$values[$field] = trim($value);
|
||||
}
|
||||
|
||||
$values['dbCharset'] = ($values['dbCharset'] === 'utf8mb4' ? 'utf8mb4' : 'utf8');
|
||||
$values['dbEngine'] = ($values['dbEngine'] === 'InnoDB' ? 'InnoDB' : 'MyISAM');
|
||||
$values['dbEngine'] = ($values['dbEngine'] === 'InnoDB' ? 'InnoDB' : 'MyISAM');
|
||||
|
||||
if(!$values['dbUser'] || !$values['dbName'] || !$values['dbPort']) {
|
||||
if(empty($values['dbUser']) || empty($values['dbName'])) {
|
||||
$this->alertErr("Missing database user and/or name");
|
||||
|
||||
$this->alertErr("Missing database configuration fields");
|
||||
} else if($values['dbCon'] === 'Socket' && empty($values['dbSocket'])) {
|
||||
$this->alertErr("Missing database socket");
|
||||
|
||||
} else if($values['dbCon'] === 'Hostname' && (empty($values['dbHost']) || empty($values['dbPort']))) {
|
||||
$this->alertErr("Missing database host and/or port");
|
||||
|
||||
} else {
|
||||
|
||||
error_reporting(0);
|
||||
|
||||
$dsn = "mysql:dbname=$values[dbName];host=$values[dbHost];port=$values[dbPort]";
|
||||
|
||||
if($values['dbCon'] === 'Socket') {
|
||||
$dsn = "mysql:unix_socket=$values[dbSocket];dbname=$values[dbName]";
|
||||
} else {
|
||||
$dsn = "mysql:dbname=$values[dbName];host=$values[dbHost];port=$values[dbPort]";
|
||||
}
|
||||
$driver_options = array(
|
||||
\PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'",
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
|
||||
@@ -830,6 +870,7 @@ class Installer {
|
||||
$file = __FILE__;
|
||||
$time = time();
|
||||
$host = empty($values['httpHosts']) ? '' : implode(',', $values['httpHosts']);
|
||||
$s = is_file("./site/config.php") ? file_get_contents("./site/config.php") : '';
|
||||
|
||||
if(function_exists('random_bytes')) {
|
||||
$authSalt = sha1(random_bytes(random_int(40, 128)));
|
||||
@@ -843,17 +884,27 @@ class Installer {
|
||||
"\n/**" .
|
||||
"\n * Installer: Database Configuration" .
|
||||
"\n * " .
|
||||
"\n */" .
|
||||
"\n */";
|
||||
|
||||
if($values['dbCon'] === 'Socket') {
|
||||
$cfg .= "\n\$config->dbSocket = '$values[dbSocket]';";
|
||||
}
|
||||
|
||||
$cfg .=
|
||||
"\n\$config->dbHost = '$values[dbHost]';" .
|
||||
"\n\$config->dbName = '$values[dbName]';" .
|
||||
"\n\$config->dbUser = '$values[dbUser]';" .
|
||||
"\n\$config->dbPass = '$values[dbPass]';" .
|
||||
"\n\$config->dbPort = '$values[dbPort]';";
|
||||
|
||||
if(!empty($values['dbCharset']) && strtolower($values['dbCharset']) != 'utf8') $cfg .= "\n\$config->dbCharset = '$values[dbCharset]';";
|
||||
if(!empty($values['dbEngine']) && $values['dbEngine'] == 'InnoDB') $cfg .= "\n\$config->dbEngine = 'InnoDB';";
|
||||
if(!empty($values['dbCharset']) && strtolower($values['dbCharset']) != 'utf8') {
|
||||
$cfg .= "\n\$config->dbCharset = '$values[dbCharset]';";
|
||||
}
|
||||
if(!empty($values['dbEngine']) && $values['dbEngine'] == 'InnoDB') {
|
||||
$cfg .= "\n\$config->dbEngine = 'InnoDB';";
|
||||
}
|
||||
|
||||
$cfg .=
|
||||
if(strpos($s, '$config->userAuthSalt') === false) $cfg .=
|
||||
"\n" .
|
||||
"\n/**" .
|
||||
"\n * Installer: User Authentication Salt " .
|
||||
@@ -864,7 +915,9 @@ class Installer {
|
||||
"\n * Do not change this value, or user passwords will no longer work." .
|
||||
"\n * " .
|
||||
"\n */" .
|
||||
"\n\$config->userAuthSalt = '$authSalt'; " .
|
||||
"\n\$config->userAuthSalt = '$authSalt'; ";
|
||||
|
||||
if(strpos($s, '$config->tableSalt') === false) $cfg .=
|
||||
"\n" .
|
||||
"\n/**" .
|
||||
"\n * Installer: Table Salt (General Purpose) " .
|
||||
@@ -874,7 +927,9 @@ class Installer {
|
||||
"\n * this value or it may break internal system comparisons that use it. " .
|
||||
"\n * " .
|
||||
"\n */" .
|
||||
"\n\$config->tableSalt = '$tableSalt'; " .
|
||||
"\n\$config->tableSalt = '$tableSalt'; ";
|
||||
|
||||
$cfg .=
|
||||
"\n" .
|
||||
"\n/**" .
|
||||
"\n * Installer: File Permission Configuration" .
|
||||
@@ -888,13 +943,17 @@ class Installer {
|
||||
"\n * " .
|
||||
"\n */" .
|
||||
"\n\$config->timezone = '$values[timezone]';" .
|
||||
"\n" .
|
||||
"\n";
|
||||
|
||||
if(strpos($s, '$config->defaultAdminTheme') === false) $cfg .=
|
||||
"\n/**" .
|
||||
"\n * Installer: Admin theme" .
|
||||
"\n * " .
|
||||
"\n */" .
|
||||
"\n\$config->defaultAdminTheme = 'AdminThemeUikit';" .
|
||||
"\n" .
|
||||
"\n";
|
||||
|
||||
if(strpos($s, '$config->installed ') === false) $cfg .=
|
||||
"\n/**" .
|
||||
"\n * Installer: Unix timestamp of date/time installed" .
|
||||
"\n * " .
|
||||
@@ -904,6 +963,17 @@ class Installer {
|
||||
"\n */" .
|
||||
"\n\$config->installed = " . time() . ";" .
|
||||
"\n\n";
|
||||
|
||||
if(strpos($s, '$config->sessionName') === false) $cfg .=
|
||||
"\n/**" .
|
||||
"\n * Installer: Session name " .
|
||||
"\n * " .
|
||||
"\n * Default session name as used in session cookie. " .
|
||||
"\n * Note that changing this will automatically logout any current sessions. " .
|
||||
"\n * " .
|
||||
"\n */" .
|
||||
"\n\$config->sessionName = 'pw" . mt_rand(0, 999) . "';" .
|
||||
"\n\n";
|
||||
|
||||
if(!empty($values['httpHosts'])) {
|
||||
$cfg .=
|
||||
@@ -927,7 +997,9 @@ class Installer {
|
||||
"\n */" .
|
||||
"\n\$config->debug = " . ($values['debugMode'] ? 'true;' : 'false;') .
|
||||
"\n\n";
|
||||
|
||||
|
||||
if(strpos($s, '<' . '?php') === false) $cfg = '<' . "?php namespace ProcessWire;\n\n" . $cfg;
|
||||
|
||||
if(($fp = fopen("./site/config.php", "a")) && fwrite($fp, $cfg)) {
|
||||
fclose($fp);
|
||||
$this->alertOk("Saved configuration to ./site/config.php");
|
||||
@@ -1072,27 +1144,28 @@ class Installer {
|
||||
*/
|
||||
protected function profileImportSQL($database, $file1, $file2, array $options = array()) {
|
||||
$defaults = array(
|
||||
'dbEngine' => 'MyISAM',
|
||||
'dbCharset' => 'utf8',
|
||||
'dbEngine' => 'InnoDB',
|
||||
'dbCharset' => 'utf8mb4',
|
||||
);
|
||||
$options = array_merge($defaults, $options);
|
||||
if(self::TEST_MODE) return;
|
||||
$restoreOptions = array();
|
||||
$replace = array();
|
||||
if($options['dbEngine'] != 'MyISAM') {
|
||||
$replace['ENGINE=MyISAM'] = "ENGINE=$options[dbEngine]";
|
||||
}
|
||||
if($options['dbCharset'] != 'utf8') {
|
||||
$replace['CHARSET=utf8'] = "CHARSET=$options[dbCharset]";
|
||||
if(strtolower($options['dbCharset']) === 'utf8mb4') {
|
||||
if(strtolower($options['dbEngine']) === 'innodb') {
|
||||
$replace['(255)'] = '(191)';
|
||||
$replace['(250)'] = '(191)';
|
||||
} else {
|
||||
$replace['(255)'] = '(250)'; // max ley length in utf8mb4 is 1000 (250 * 4)
|
||||
}
|
||||
$replace['ENGINE=InnoDB'] = "ENGINE=$options[dbEngine]";
|
||||
$replace['ENGINE=MyISAM'] = "ENGINE=$options[dbEngine]";
|
||||
$replace['CHARSET=utf8mb4;'] = "CHARSET=$options[dbCharset];";
|
||||
$replace['CHARSET=utf8;'] = "CHARSET=$options[dbCharset];";
|
||||
$replace['CHARSET=utf8 COLLATE='] = "CHARSET=$options[dbCharset] COLLATE=";
|
||||
|
||||
if(strtolower($options['dbCharset']) === 'utf8mb4') {
|
||||
if(strtolower($options['dbEngine']) === 'innodb') {
|
||||
$replace['(255)'] = '(191)';
|
||||
$replace['(250)'] = '(191)';
|
||||
} else {
|
||||
$replace['(255)'] = '(250)'; // max ley length in utf8mb4 is 1000 (250 * 4)
|
||||
}
|
||||
}
|
||||
|
||||
if(count($replace)) $restoreOptions['findReplaceCreateTable'] = $replace;
|
||||
require("./wire/core/WireDatabaseBackup.php");
|
||||
$backup = new WireDatabaseBackup();
|
||||
@@ -1196,6 +1269,7 @@ class Installer {
|
||||
);
|
||||
|
||||
foreach($this->findProfiles() as $name => $profile) {
|
||||
if($name === 'site') continue;
|
||||
$title = empty($profile['title']) ? $name : $profile['title'];
|
||||
$items[$name] = array(
|
||||
'label' => "Remove unused $title site profile (/$name/)",
|
||||
@@ -1772,10 +1846,6 @@ class Installer {
|
||||
|
||||
if($value === null && empty($sanitizer)) return null;
|
||||
|
||||
if(version_compare(PHP_VERSION, "5.4.0", "<") && function_exists('get_magic_quotes_gpc')) {
|
||||
if(get_magic_quotes_gpc()) $value = stripslashes($value);
|
||||
}
|
||||
|
||||
switch($sanitizer) {
|
||||
case 'intSigned':
|
||||
$value = (int) $value;
|
||||
@@ -2002,7 +2072,6 @@ class Installer {
|
||||
/****************************************************************************************************/
|
||||
|
||||
if(!Installer::TEST_MODE && is_file("./site/assets/installed.php")) die("This installer has already run. Please delete it.");
|
||||
error_reporting(E_ALL | E_STRICT);
|
||||
error_reporting(E_ALL);
|
||||
$installer = new Installer();
|
||||
$installer->execute();
|
||||
|
||||
$installer->execute();
|
@@ -17,7 +17,7 @@
|
||||
* This file is licensed under the MIT license
|
||||
* https://processwire.com/about/license/mit/
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*/
|
||||
|
@@ -12,7 +12,7 @@
|
||||
* You may also make up your own configuration options by assigning them
|
||||
* in /site/config.php
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*
|
||||
@@ -523,6 +523,18 @@ $config->sessionHistory = 0;
|
||||
*/
|
||||
$config->userAuthHashType = 'sha1';
|
||||
|
||||
/**
|
||||
* Enable output formatting for current $user API variable at boot?
|
||||
*
|
||||
* EXPERIMENTAL: May not be compatible with with all usages, so if setting to `true`
|
||||
* then be sure to test thoroughly on anything that works with $user API variable.
|
||||
*
|
||||
* @var bool
|
||||
* @since 3.0.241
|
||||
*
|
||||
*/
|
||||
$config->userOutputFormatting = false;
|
||||
|
||||
/**
|
||||
* Names (string) or IDs (int) of roles that are not allowed to login
|
||||
*
|
||||
@@ -973,7 +985,7 @@ $config->protectCSRF = true;
|
||||
* @var int
|
||||
*
|
||||
*/
|
||||
$config->maxUrlSegments = 4;
|
||||
$config->maxUrlSegments = 20;
|
||||
|
||||
/**
|
||||
* Maximum length for any individual URL segment (default=128)
|
||||
@@ -992,7 +1004,23 @@ $config->maxUrlSegmentLength = 128;
|
||||
* @var int
|
||||
*
|
||||
*/
|
||||
$config->maxUrlDepth = 30;
|
||||
$config->maxUrlDepth = 30;
|
||||
|
||||
/**
|
||||
* Long URL response (URL depth, length or segments overflow)
|
||||
*
|
||||
* HTTP code that ProcessWire should respond with when it receives more URL segments,
|
||||
* more URL depth, or longer URL length than what is allowed. Suggested values:
|
||||
*
|
||||
* - `404`: Page not found
|
||||
* - `301`: Redirect to closest allowed URL (permanent)
|
||||
* - `302`: Redirect to closest allowed URL (temporary)
|
||||
*
|
||||
* @var int
|
||||
* @since 3.0.243
|
||||
*
|
||||
*/
|
||||
$config->longUrlResponse = 404;
|
||||
|
||||
/**
|
||||
* Pagination URL prefix
|
||||
@@ -1037,6 +1065,14 @@ $config->pageNameCharset = 'ascii';
|
||||
*
|
||||
* Please note this whitelist is only used if pageNameCharset is 'UTF8'.
|
||||
*
|
||||
* If your ProcessWire version is 3.0.244+ AND your installation date was before 10 Jan 2025,
|
||||
* AND you are enabling UTF8 page names now, please add the text `v3` (without the quotes)
|
||||
* at the beginning or end of your pageNameWhitelist. This will ensure that it uses a
|
||||
* newer/better UTF-8 page name conversion. The older version is buggy on PHP versions 7.4+,
|
||||
* but is used for existing installations so as not to unexpectedly change any existing page
|
||||
* names. When a new ProcessWire installation occurs after 5 Jan 2025 it automatically uses
|
||||
* the newer/better version and does not require anything further.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
*/
|
||||
@@ -1405,8 +1441,8 @@ $config->moduleServiceKey = 'pw301';
|
||||
*/
|
||||
$config->moduleInstall = array(
|
||||
'directory' => 'debug', // allow install from ProcessWire modules directory?
|
||||
'upload' => 'debug', // allow install by module file upload?
|
||||
'download' => 'debug', // allow install by download from URL?
|
||||
'upload' => false, // allow install by module file upload?
|
||||
'download' => false, // allow install by download from URL?
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -1612,6 +1648,8 @@ $config->adminEmail = '';
|
||||
* #property bool compress Compress compiled CSS?
|
||||
* #property array customLessFiles Custom .less files to include, relative to PW installation root.
|
||||
* #property string customCssFile Target custom .css file to compile custom .less file(s) to.
|
||||
* #property bool noDarkMode If theme supports a dark mode, specify true to disable it as an option.
|
||||
* #property bool noTogcbx If theme supports toggle style checkboxes, disable them.
|
||||
*
|
||||
*/
|
||||
$config->AdminThemeUikit = array(
|
||||
@@ -1620,6 +1658,8 @@ $config->AdminThemeUikit = array(
|
||||
'compress' => true,
|
||||
'customLessFiles' => array('/site/templates/admin.less'),
|
||||
'customCssFile' => '/site/assets/admin.css',
|
||||
'noDarkMode' => false,
|
||||
'noTogcbx' => false,
|
||||
);
|
||||
|
||||
/**
|
||||
|
@@ -193,10 +193,16 @@ abstract class AdminThemeFramework extends AdminTheme {
|
||||
*
|
||||
*/
|
||||
public function getHeadline() {
|
||||
$headline = $this->wire('processHeadline');
|
||||
if(!$headline) $headline = $this->wire()->page->get('title|name');
|
||||
$headline = (string) $this->wire('processHeadline');
|
||||
if(!strlen($headline)) $headline = $this->wire()->page->get('title|name');
|
||||
if($headline !== 'en' && $this->wire()->languages) $headline = $this->_($headline);
|
||||
return $this->sanitizer->entities1($headline);
|
||||
$headline = $this->sanitizer->entities1($headline);
|
||||
if(strpos($headline, '<icon-') !== false && !$this->wire()->process instanceof WirePageEditor) {
|
||||
if(preg_match('/<icon-([-a-z0-9]+)>/', $headline, $matches)) {
|
||||
$headline = str_replace($matches[0], wireIconMarkup($matches[1]), $headline);
|
||||
}
|
||||
}
|
||||
return $headline;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -543,7 +549,19 @@ abstract class AdminThemeFramework extends AdminTheme {
|
||||
*
|
||||
* This is hookable so that something else could add stuff to it.
|
||||
* See the method body for details on format used.
|
||||
*
|
||||
*
|
||||
* Supported properties/attributes as of 3.0.248:
|
||||
*
|
||||
* - url (href)
|
||||
* - title (label text)
|
||||
* - target (html attr)
|
||||
* - icon (name of icon)
|
||||
* - permission (required permission)
|
||||
* - id (html attr)
|
||||
* - class (html attr)
|
||||
* - onclick (html attr)
|
||||
* - data-* (html attr)
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
@@ -600,6 +618,10 @@ abstract class AdminThemeFramework extends AdminTheme {
|
||||
if(strpos($httpHost, ':')) $httpHost = preg_replace('/:\d+/', '', $httpHost); // remove port
|
||||
$browserTitle .= " • $httpHost";
|
||||
}
|
||||
|
||||
if(strpos($browserTitle, '<icon-') !== false) {
|
||||
$browserTitle = preg_replace('/<icon-[-a-z0-9]+>\s*/', '', $browserTitle);
|
||||
}
|
||||
|
||||
return $this->sanitizer->entities1($browserTitle);
|
||||
}
|
||||
@@ -676,7 +698,7 @@ abstract class AdminThemeFramework extends AdminTheme {
|
||||
foreach($notices as $n => $notice) {
|
||||
/** @var Notice $notice */
|
||||
|
||||
$text = $notice->text;
|
||||
$text = (string) $notice->text;
|
||||
$allowMarkup = $notice->flags & Notice::allowMarkup;
|
||||
$groupByType = $options['groupByType'] && !($notice->flags & Notice::noGroup) && !($notice instanceof NoticeError);
|
||||
|
||||
@@ -799,8 +821,7 @@ abstract class AdminThemeFramework extends AdminTheme {
|
||||
*
|
||||
*/
|
||||
public function renderExtraMarkup($for) {
|
||||
static $extras = array();
|
||||
if(empty($extras)) $extras = $this->getExtraMarkup();
|
||||
$extras = $this->getExtraMarkup();
|
||||
return isset($extras[$for]) ? $extras[$for] : '';
|
||||
}
|
||||
|
||||
|
@@ -45,7 +45,7 @@
|
||||
* @property array $imageSizerOptions Options to set image sizing defaults. Please see the /wire/config.php file for all options and defaults. #pw-group-images
|
||||
* @property array $webpOptions Options for webp images. Please see /wire/config.php for all options. #pw-group-images
|
||||
*
|
||||
* @property bool $pagefileSecure When used, files in /site/assets/files/ will be protected with the same access as the page. Routines files through a passthrough script. #pw-group-files
|
||||
* @property bool $pagefileSecure When used, files in /site/assets/files/ will be protected with the same access as the page. Routes files through a passthrough script. Note if applying to existing site it may not affect existing pages and file/image fields until they are accessed or saved. #pw-group-files
|
||||
* @property string $pagefileSecurePathPrefix One or more characters prefixed to the pathname of protected file dirs. This should be some prefix that the .htaccess file knows to block requests for. #pw-group-files
|
||||
* @property string $pagefileUrlPrefix Deprecated property that was a string that prefixes filenames in PW URLs, becoming a shortcut to a page’s file’s URL (do not use, here for backwards compatibility only). #pw-internal
|
||||
*
|
||||
@@ -91,6 +91,7 @@
|
||||
* @property int $maxUrlSegments Maximum number of extra stacked URL segments allowed in a page's URL (including page numbers) #pw-group-URLs
|
||||
* @property int $maxUrlSegmentLength Maximum length of any individual URL segment (default=128). #pw-group-URLs
|
||||
* @property int $maxUrlDepth Maximum URL/path slashes (depth) for request URLs. (Min=10, Max=60) #pw-group-URLs
|
||||
* @property int $longUrlResponse Response code when URL segments, depth or length exceeds max allowed. #pw-group-URLs @since 3.0.243
|
||||
* @property string $wireInputOrder Order that variables with the $input API var are handled when you access $input->var. #pw-group-HTTP-and-input
|
||||
* @property bool $wireInputLazy Specify true for $input API var to load input data in a lazy fashion and potentially use less memory. Default is false. #pw-group-HTTP-and-input
|
||||
* @property int $wireInputArrayDepth Maximum multi-dimensional array depth for input variables accessed from $input or 1 to only allow single dimension arrays. #pw-group-HTTP-and-input @since 3.0.178
|
||||
@@ -156,6 +157,7 @@
|
||||
*
|
||||
* @property string $userAuthSalt Salt generated at install time to be used as a secondary/non-database salt for the password system. #pw-group-session
|
||||
* @property string $userAuthHashType Default is 'sha1' - used only if Blowfish is not supported by the system. #pw-group-session
|
||||
* @property bool $userOutputFormatting Enable output formatting for current $user API variable at boot? (default=false) #pw-group-session @since 3.0.241
|
||||
* @property string $tableSalt Additional hash for other (non-authentication) purposes. #pw-group-system @since 3.0.164
|
||||
*
|
||||
* @property bool $internal This is automatically set to FALSE when PW is externally bootstrapped. #pw-group-runtime
|
||||
|
@@ -80,7 +80,7 @@ class Database extends \mysqli implements WireDatabase {
|
||||
|
||||
if($config) {
|
||||
if($config->dbCharset) $this->set_charset($config->dbCharset);
|
||||
else if($config->dbSetNamesUTF8) $this->query("SET NAMES 'utf8'");
|
||||
else if($config->get('dbSetNamesUTF8')) $this->query("SET NAMES 'utf8'");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,12 +134,12 @@ class Database extends \mysqli implements WireDatabase {
|
||||
*
|
||||
* Active in ProcessWire debug mode only
|
||||
*
|
||||
* @param ProcessWire $wire ProcessWire instance, if omitted returns queries for all instances
|
||||
* @param ProcessWire|null $wire ProcessWire instance, if omitted returns queries for all instances
|
||||
* @return array
|
||||
* @deprecated
|
||||
*
|
||||
*/
|
||||
static public function getQueryLog(ProcessWire $wire = null) {
|
||||
static public function getQueryLog(?ProcessWire $wire = null) {
|
||||
if($wire) {
|
||||
return $wire->database->queryLog();
|
||||
} else {
|
||||
|
@@ -465,7 +465,7 @@ abstract class DatabaseQuery extends WireData {
|
||||
if(is_array($value)) {
|
||||
$curValue = array_merge($curValue, $value);
|
||||
} else {
|
||||
$curValue[] = trim($value, ", ");
|
||||
$curValue[] = trim("$value", ", ");
|
||||
}
|
||||
|
||||
$this->set($method, $curValue);
|
||||
@@ -745,15 +745,10 @@ abstract class DatabaseQuery extends WireData {
|
||||
|
||||
if($exception && $options['throw']) {
|
||||
if($this->wire()->config->allowExceptions) throw $exception; // throw original
|
||||
$message = (string) $exception->getMessage();
|
||||
$code = (int) $exception->getCode();
|
||||
// note: re-throw below complains about wrong arguments if the above two
|
||||
// lines are called in the line below, so variables are intermediary
|
||||
throw new WireDatabaseQueryException($message, $code, $exception);
|
||||
WireException([ 'class' => 'WireDatabaseQueryException', 'previous' => $exception ]);
|
||||
}
|
||||
|
||||
return $options['returnQuery'] ? $query : $result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -245,7 +245,7 @@ class DatabaseQuerySelectFulltext extends Wire {
|
||||
*
|
||||
*/
|
||||
protected function escapeLike($str) {
|
||||
return str_replace(array('%', '_'), array('\\%', '\\_'), $str);
|
||||
return str_replace(array('%', '_'), array('\\%', '\\_'), "$str");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,7 +258,7 @@ class DatabaseQuerySelectFulltext extends Wire {
|
||||
*
|
||||
*/
|
||||
protected function escapeAgainst($str) {
|
||||
$str = str_replace(array('@', '+', '-', '*', '~', '<', '>', '(', ')', ':', '"', '&', '|', '=', '.'), ' ', $str);
|
||||
$str = str_replace(array('@', '+', '-', '*', '~', '<', '>', '(', ')', ':', '"', '&', '|', '=', '.'), ' ', "$str");
|
||||
while(strpos($str, ' ')) $str = str_replace(' ', ' ', $str);
|
||||
return $str;
|
||||
}
|
||||
@@ -270,7 +270,7 @@ class DatabaseQuerySelectFulltext extends Wire {
|
||||
*/
|
||||
protected function value($value) {
|
||||
$maxLength = self::maxQueryValueLength;
|
||||
$value = trim($value);
|
||||
$value = trim("$value");
|
||||
if(strlen($value) < $maxLength && strpos($value, "\n") === false && strpos($value, "\r") === false) return $value;
|
||||
$value = $this->sanitizer->trunc($value, $maxLength);
|
||||
return $value;
|
||||
@@ -328,6 +328,10 @@ class DatabaseQuerySelectFulltext extends Wire {
|
||||
} else {
|
||||
$this->matchFieldName($fieldName, $value);
|
||||
}
|
||||
|
||||
if(!count($this->query->where) && (strpos($operator, '~') !== false || $operator === '*+=')) {
|
||||
$this->query->where('(1>2)'); // force non-match
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -720,9 +724,10 @@ class DatabaseQuerySelectFulltext extends Wire {
|
||||
$wordsAlternates = array();
|
||||
|
||||
$phraseWords = $this->words($value); // including non-indexable
|
||||
$lastPhraseWord = array_pop($phraseWords);
|
||||
$lastPhraseWord = (string) array_pop($phraseWords);
|
||||
$scoreField = $this->getScoreFieldName();
|
||||
$againstValues = array();
|
||||
$matchAgainst = null;
|
||||
|
||||
// BOOLEAN PHRASE: full phrase matches come before expanded matches
|
||||
if(count($phraseWords)) {
|
||||
@@ -748,19 +753,20 @@ class DatabaseQuerySelectFulltext extends Wire {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$againstValues[] = ($this->isIndexableWord($lastPhraseWord) ? '+' : '') . $this->escapeAgainst($lastPhraseWord) . '*';
|
||||
$bindKey = $this->query->bindValueGetKey(implode(' ', $againstValues));
|
||||
$matchAgainst = "$matchType($tableField) AGAINST($bindKey IN BOOLEAN MODE)";
|
||||
|
||||
if($this->allowOrder) {
|
||||
$this->query->select("$matchAgainst + 333.3 AS $scoreField");
|
||||
$this->query->orderby("$scoreField DESC");
|
||||
|
||||
if(strlen($lastPhraseWord)) {
|
||||
$againstValues[] = ($this->isIndexableWord($lastPhraseWord) ? '+' : '') . $this->escapeAgainst($lastPhraseWord) . '*';
|
||||
$bindKey = $this->query->bindValueGetKey(implode(' ', $againstValues));
|
||||
$matchAgainst = "$matchType($tableField) AGAINST($bindKey IN BOOLEAN MODE)";
|
||||
if($this->allowOrder) {
|
||||
$this->query->select("$matchAgainst + 333.3 AS $scoreField");
|
||||
$this->query->orderby("$scoreField DESC");
|
||||
}
|
||||
}
|
||||
|
||||
if(!count($words)) {
|
||||
// no words to work with for query expansion (not likely, unless stopwords or too-short)
|
||||
$this->query->where($matchAgainst);
|
||||
if($matchAgainst) $this->query->where($matchAgainst);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1215,6 +1221,7 @@ class DatabaseQuerySelectFulltext extends Wire {
|
||||
'stopwords' => true, // allow stopwords
|
||||
'indexable' => false, // include only indexable words?
|
||||
'alternates' => false, // include alternate versions of words?
|
||||
'truncate' => true,
|
||||
);
|
||||
|
||||
$options = count($options) ? array_merge($defaults, $options) : $defaults;
|
||||
@@ -1269,7 +1276,7 @@ class DatabaseQuerySelectFulltext extends Wire {
|
||||
|
||||
if(strpos($likeValue, "'") !== false || strpos($likeValue, "’") !== false) {
|
||||
// match either straight or curly apostrophe
|
||||
$likeValue = preg_replace('/[\'’]+/', '(\'|’)', $likeValue);
|
||||
$likeValue = str_replace([ "'", "’" ], "('|’)", $likeValue);
|
||||
// if word ends with apostrophe then apostrophe is optional
|
||||
$likeValue = rtrim(str_replace("('|’) ", "('|’)? ", "$likeValue "));
|
||||
}
|
||||
@@ -1312,6 +1319,7 @@ class DatabaseQuerySelectFulltext extends Wire {
|
||||
*
|
||||
*/
|
||||
protected function strlen($value) {
|
||||
$value = (string) $value;
|
||||
if(function_exists('mb_strlen')) {
|
||||
return mb_strlen($value);
|
||||
} else {
|
||||
|
@@ -376,7 +376,7 @@ class Debug {
|
||||
$obj = null;
|
||||
$class = '';
|
||||
$type = '';
|
||||
$args = $trace['args'];
|
||||
$args = isset($trace['args']) ? $trace['args'] : array();
|
||||
$argStr = '';
|
||||
$file = $trace['file'];
|
||||
$basename = basename($file);
|
||||
|
@@ -8,36 +8,133 @@
|
||||
* This file is licensed under the MIT license
|
||||
* https://processwire.com/about/license/mit/
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Throw a new WireException functionally
|
||||
*
|
||||
* This can be used to facilitiate re-throwing a non-WireException as a WireException,
|
||||
* notably \PDOException or other exception classes that might use string for `code` property.
|
||||
*
|
||||
* ~~~~
|
||||
* // throw random WireException
|
||||
* WireException();
|
||||
*
|
||||
* // throw with message
|
||||
* WireException('Hello world');
|
||||
*
|
||||
* // throw WirePermissionException
|
||||
* WireException([ 'class' => 'WirePermissionException', 'message' => 'No access' ]);
|
||||
*
|
||||
* // re-throw previous exception as WireException (and inherit message and code)
|
||||
* WireException([ 'previous' => $exception ]);
|
||||
* ~~~~
|
||||
*
|
||||
* @param array|string $options One of the following options, or string for just `message`:
|
||||
* - `class` (string): Class name of WireException to throw (default='WireException').
|
||||
* - `message` (string): Exception message string (default='' or pulled from previous exception).
|
||||
* - `code` (int|string): Exception code integer or alphanumeric string (default=0 or pulled from previous exception).
|
||||
* - `previous` (\Throwable): Previous exception. When present, code and message will be pulled from it if not specified.
|
||||
* @throws WireException
|
||||
* @since 3.0.248
|
||||
*
|
||||
*
|
||||
*/
|
||||
function WireException($options = []) {
|
||||
$defaults = [
|
||||
'class' => 'WireException',
|
||||
'message' => is_string($options) ? $options : '',
|
||||
'code' => 0,
|
||||
'previous' => null,
|
||||
];
|
||||
$options = is_array($options) ? array_merge($defaults, $options) : $defaults;
|
||||
if($options['previous'] instanceof \Throwable) {
|
||||
if(empty($options['message'])) {
|
||||
$options['message'] = $options['previous']->getMessage();
|
||||
}
|
||||
if(empty($options['code'])) {
|
||||
$options['code'] = $options['previous']->getCode();
|
||||
}
|
||||
} else {
|
||||
$options['previous'] = null;
|
||||
}
|
||||
$class = wireClassName($options['class'], true);
|
||||
$e = new $class($options['message'], 0, $options['previous']);
|
||||
if($e instanceof WireException && $options['code'] !== 0) {
|
||||
$e->setCode($options['code']);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic ProcessWire exception
|
||||
*
|
||||
*/
|
||||
class WireException extends \Exception {
|
||||
|
||||
/**
|
||||
* Exception code when a string
|
||||
*
|
||||
* @var string
|
||||
* @since 3.0.248
|
||||
*
|
||||
*/
|
||||
protected $codeStr = '';
|
||||
|
||||
/**
|
||||
* Replace previously set message
|
||||
*
|
||||
* Public since 3.0.248
|
||||
*
|
||||
* @param string $message
|
||||
* @since 3.0.150
|
||||
*
|
||||
*/
|
||||
protected function setMessage($message) {
|
||||
public function setMessage($message) {
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace previously set code
|
||||
*
|
||||
* @param int $code
|
||||
* Public since 3.0.248
|
||||
*
|
||||
* @param int|string $code
|
||||
* @since 3.0.150
|
||||
*
|
||||
*/
|
||||
protected function setCode($code) {
|
||||
$this->code = $code;
|
||||
public function setCode($code) {
|
||||
if(is_string($code)) {
|
||||
$this->setCodeStr($code);
|
||||
if(ctype_digit($code)) $this->code = (int) $code;
|
||||
} else {
|
||||
$this->code = (int) $code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set alphanumeric code string
|
||||
*
|
||||
* @param string $codeStr
|
||||
* @since 3.0.248
|
||||
*
|
||||
*/
|
||||
public function setCodeStr($codeStr) {
|
||||
$this->codeStr = (string) $codeStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get alphanumeric/string code if set, blank string if not
|
||||
*
|
||||
* @return string
|
||||
* @since 3.0.248
|
||||
*
|
||||
*/
|
||||
public function getCodeStr() {
|
||||
return $this->codeStr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +223,7 @@ class WireDatabaseException extends WireException {}
|
||||
*
|
||||
* May have \PDOException populated with call to its getPrevious(); method,
|
||||
* in which can it also has same getCode() and getMessage() as \PDOException.
|
||||
* Use getCodeStr() for PDOException string code.
|
||||
*
|
||||
* @since 3.0.156
|
||||
*
|
||||
@@ -169,5 +267,3 @@ class PageFinderException extends WireException { }
|
||||
*
|
||||
*/
|
||||
class PageFinderSyntaxException extends PageFinderException { }
|
||||
|
||||
|
||||
|
@@ -879,7 +879,7 @@ class Field extends WireData implements Saveable, Exportable {
|
||||
* @return bool True if viewable, false if not
|
||||
*
|
||||
*/
|
||||
public function ___viewable(Page $page = null, User $user = null) {
|
||||
public function ___viewable(?Page $page = null, ?User $user = null) {
|
||||
return $this->wire()->fields->_hasPermission($this, 'view', $page, $user);
|
||||
}
|
||||
|
||||
@@ -893,12 +893,12 @@ class Field extends WireData implements Saveable, Exportable {
|
||||
*
|
||||
* #pw-group-access
|
||||
*
|
||||
* @param Page|string|int|null $page Optionally specify a Page for context
|
||||
* @param User|string|int|null $user Optionally specify a different user (default = current user)
|
||||
* @param Page|null $page Optionally specify a Page for context
|
||||
* @param User|null $user Optionally specify a different user (default = current user)
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function ___editable(Page $page = null, User $user = null) {
|
||||
public function ___editable(?Page $page = null, ?User $user = null) {
|
||||
return $this->wire()->fields->_hasPermission($this, 'edit', $page, $user);
|
||||
}
|
||||
|
||||
@@ -1065,7 +1065,13 @@ class Field extends WireData implements Saveable, Exportable {
|
||||
foreach(array('showIf', 'requiredIf') as $depType) {
|
||||
$theIf = $inputfield->getSetting($depType);
|
||||
if(empty($theIf)) continue;
|
||||
$inputfield->set($depType, preg_replace('/([_.|a-zA-Z0-9]+)([=!%*<>]+)/', '$1' . $contextStr . '$2', $theIf));
|
||||
$theIf = preg_replace('/([_|a-zA-Z0-9]+)*([-._|a-zA-Z0-9]*)([=!%*<>]+)/', '$1' . $contextStr . '$2$3', $theIf);
|
||||
if(stripos($theIf, 'forpage.') !== false) {
|
||||
// de-contextualize if the field name starts with 'forpage.' as used by
|
||||
// repeaters (or others) referring to page in editor rather than item page
|
||||
$theIf = preg_replace('/forpage\.([_.|a-z0-9]+)' . $contextStr . '([=!%*<>]+)/i', '$1$2', $theIf);
|
||||
}
|
||||
$inputfield->set($depType, $theIf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1242,7 +1248,9 @@ class Field extends WireData implements Saveable, Exportable {
|
||||
$table = $this->setTable;
|
||||
} else {
|
||||
$name = $this->settings['name'];
|
||||
if(!strlen($name)) throw new WireException("Field 'name' is required");
|
||||
$length = strlen($name);
|
||||
if(!$length) throw new WireException("Field 'name' is required");
|
||||
if($length > 58) $name = substr($name, 0, 58); // 'field_' + 58 = 64 max
|
||||
$table = self::tablePrefix . $name;
|
||||
}
|
||||
if(self::$lowercaseTables) $table = strtolower($table);
|
||||
@@ -1259,6 +1267,7 @@ class Field extends WireData implements Saveable, Exportable {
|
||||
*/
|
||||
public function setTable($table = null) {
|
||||
$table = empty($table) ? '' : $this->wire()->sanitizer->fieldName($table);
|
||||
if(strlen($table) > 64) $table = substr($table, 0, 64);
|
||||
$this->setTable = $table;
|
||||
}
|
||||
|
||||
|
@@ -82,7 +82,10 @@ class FieldSelectorInfo extends Wire {
|
||||
// when input=select, page or checkbox, this contains the selectable options (value => label)
|
||||
'options' => array(),
|
||||
// if field has subfields, this contains array of all above, indexed by subfield name (blank if not applicable)
|
||||
'subfields' => array(),
|
||||
'subfields' => array(
|
||||
// same as above, plus… DB column name (if different from 'name')
|
||||
// 'col' => '',
|
||||
),
|
||||
);
|
||||
|
||||
$this->schemaToInput = array(
|
||||
|
@@ -518,6 +518,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
|
||||
* - `namespace` (string): Additional namespace for Inputfield context.
|
||||
* - `flat` (bool): Return all Inputfields in a flattened InputfieldWrapper?
|
||||
* - `populate` (bool): Populate page values to Inputfields? (default=true) since 3.0.208
|
||||
* - `container` (InputfieldWrapper): The InputfieldWrapper element to add fields into, or omit for new. since 3.0.239
|
||||
* @param string|array $fieldName Limit to a particular fieldName(s) or field IDs (optional).
|
||||
* - If specifying a single field (name or ID) and it refers to a fieldset, then all fields in that fieldset will be included.
|
||||
* - If specifying an array of field names/IDs the returned InputfieldWrapper will maintain the requested order.
|
||||
@@ -536,6 +537,7 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
|
||||
'namespace' => $namespace,
|
||||
'flat' => $flat,
|
||||
'populate' => true, // populate page values?
|
||||
'container' => null,
|
||||
);
|
||||
$options = $contextStr;
|
||||
$options = array_merge($defaults, $options);
|
||||
@@ -544,11 +546,16 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
|
||||
$namespace = $options['namespace'];
|
||||
$populate = (bool) $options['populate'];
|
||||
$flat = $options['flat'];
|
||||
$container = $options['container'];
|
||||
} else {
|
||||
$populate = true;
|
||||
$container = null;
|
||||
}
|
||||
|
||||
if(!$container instanceof InputfieldWrapper) {
|
||||
$container = $this->wire(new InputfieldWrapper());
|
||||
}
|
||||
|
||||
$container = $this->wire(new InputfieldWrapper());
|
||||
$containers = array();
|
||||
$inFieldset = false;
|
||||
$inHiddenFieldset = false;
|
||||
@@ -777,5 +784,3 @@ class Fieldgroup extends WireArray implements Saveable, Exportable, HasLookupIte
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@@ -7,7 +7,7 @@
|
||||
* #pw-body For full details on all methods available in a Fieldgroup, be sure to also see the `WireArray` class.
|
||||
* #pw-var $fieldgroups
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @method Fieldgroup clone(Saveable $item, $name = '')
|
||||
@@ -349,19 +349,46 @@ class Fieldgroups extends WireSaveableItemsLookup {
|
||||
*
|
||||
* @param Saveable $item Item to clone
|
||||
* @param string $name
|
||||
* @return bool|Saveable $item Returns the new clone on success, or false on failure
|
||||
* @return Saveable|Fieldgroup
|
||||
* @return Fieldgroup|false $item Returns the new clone on success, or false on failure
|
||||
*
|
||||
*/
|
||||
public function ___clone(Saveable $item, $name = '') {
|
||||
return parent::___clone($item, $name);
|
||||
// @TODO clone the field context data
|
||||
/*
|
||||
$id = $item->id;
|
||||
$item = parent::___clone($item);
|
||||
if(!$item) return false;
|
||||
return $item;
|
||||
*/
|
||||
if(!$item instanceof Fieldgroup) return false;
|
||||
|
||||
$database = $this->wire()->database;
|
||||
|
||||
/** @var Fieldgroup|false $fieldgroup */
|
||||
$fieldgroup = parent::___clone($item, $name);
|
||||
if(!$fieldgroup) return false;
|
||||
|
||||
$sql =
|
||||
'SELECT fields_id, sort, data FROM fieldgroups_fields ' .
|
||||
'WHERE fieldgroups_id=:fieldgroups_id ' .
|
||||
'AND data IS NOT NULL';
|
||||
|
||||
$query = $this->wire()->database->prepare($sql);
|
||||
$query->bindValue(':fieldgroups_id', $item->id, \PDO::PARAM_INT);
|
||||
$query->execute();
|
||||
|
||||
$rows = $query->fetchAll(\PDO::FETCH_ASSOC);
|
||||
$query->closeCursor();
|
||||
|
||||
$sql =
|
||||
'UPDATE fieldgroups_fields SET data=:data ' .
|
||||
'WHERE fieldgroups_id=:fieldgroups_id ' .
|
||||
'AND fields_id=:fields_id AND sort=:sort';
|
||||
|
||||
$query = $database->prepare($sql);
|
||||
|
||||
foreach($rows as $row) {
|
||||
$query->bindValue(':data', $row['data']);
|
||||
$query->bindValue(':fieldgroups_id', (int) $fieldgroup->id, \PDO::PARAM_INT);
|
||||
$query->bindValue(':fields_id', (int) $row['fields_id'], \PDO::PARAM_INT);
|
||||
$query->bindValue(':sort', (int) $row['sort'], \PDO::PARAM_INT);
|
||||
$query->execute();
|
||||
}
|
||||
|
||||
return $fieldgroup;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -566,12 +593,12 @@ class Fieldgroups extends WireSaveableItemsLookup {
|
||||
* #pw-internal
|
||||
*
|
||||
* @param Field $field
|
||||
* @param Template $template
|
||||
* @param Fieldgroup $fieldgroup
|
||||
* @param Template|null $template
|
||||
* @return bool|string Returns error message string if not removeable or boolean false if it is removeable
|
||||
*
|
||||
*/
|
||||
public function isFieldNotRemoveable(Field $field, Fieldgroup $fieldgroup, Template $template = null) {
|
||||
public function isFieldNotRemoveable(Field $field, Fieldgroup $fieldgroup, ?Template $template = null) {
|
||||
|
||||
if(is_null($template)) $template = $this->wire()->templates->get($fieldgroup->name);
|
||||
|
||||
@@ -619,4 +646,3 @@ class Fieldgroups extends WireSaveableItemsLookup {
|
||||
*/
|
||||
public function ___fieldRemoved(Fieldgroup $fieldgroup, Field $field) { }
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
*
|
||||
* WireArray of Fieldgroup instances as used by Fieldgroups class.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*
|
||||
@@ -25,6 +25,7 @@ class FieldgroupsArray extends WireArray {
|
||||
*
|
||||
*/
|
||||
public function getItemKey($item) {
|
||||
/** @var Fieldgroup $item */
|
||||
return $item->id;
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
*
|
||||
* Manages collection of ALL Field instances, not specific to any particular Fieldgroup
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* #pw-summary Manages all custom fields in ProcessWire, independently of any Fieldgroup.
|
||||
@@ -107,6 +107,13 @@ class Fields extends WireSaveableItems {
|
||||
*/
|
||||
protected $flagNames = array();
|
||||
|
||||
/**
|
||||
* Flags to field IDs
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $flagsToIds = array();
|
||||
|
||||
/**
|
||||
* Field names that are native/permanent to this instance of ProcessWire (configurable at runtime)
|
||||
*
|
||||
@@ -184,6 +191,50 @@ class Fields extends WireSaveableItems {
|
||||
return $this->wire(new Field());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after rows loaded from DB but before populated to this instance
|
||||
*
|
||||
* @param array $rows
|
||||
*
|
||||
*/
|
||||
protected function loadRowsReady(array &$rows) {
|
||||
for($flag = 1; $flag <= 256; $flag *= 2) {
|
||||
$this->flagsToIds[$flag] = array();
|
||||
}
|
||||
foreach($rows as $row) {
|
||||
$flags = (int) $row['flags'];
|
||||
if(empty($flags)) continue;
|
||||
foreach($this->flagsToIds as $flag => $ids) {
|
||||
if($flags & $flag) $this->flagsToIds[$flag][] = (int) $row['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given field ID return native property
|
||||
*
|
||||
* This avoids loading the field if the property can be obtained natively.
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param int $id
|
||||
* @param string $property
|
||||
* @return array|bool|mixed|string|null
|
||||
* @since 3.0.243
|
||||
*
|
||||
*/
|
||||
public function fieldIdToProperty($id, $property) {
|
||||
$id = (int) $id;
|
||||
if(isset($this->lazyIdIndex[$id])) {
|
||||
$n = $this->lazyIdIndex[$id];
|
||||
if(isset($this->lazyItems[$n][$property])) {
|
||||
return $this->lazyItems[$n][$property];
|
||||
}
|
||||
}
|
||||
$field = $this->get($id);
|
||||
return $field ? $field->get($property) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an item and populate with given data
|
||||
*
|
||||
@@ -250,7 +301,7 @@ class Fields extends WireSaveableItems {
|
||||
* @since 3.0.194
|
||||
*
|
||||
*/
|
||||
protected function initItem(array &$row, WireArray $items = null) {
|
||||
protected function initItem(array &$row, ?WireArray $items = null) {
|
||||
/** @var Field $item */
|
||||
$item = parent::initItem($row, $items);
|
||||
$fieldtype = $item ? $item->type : null;
|
||||
@@ -317,7 +368,7 @@ class Fields extends WireSaveableItems {
|
||||
* $fields->save($field);
|
||||
* ~~~~~
|
||||
*
|
||||
* @param Field|Saveable $item The field to save
|
||||
* @param Field $item The field to save
|
||||
* @return bool True on success, false on failure
|
||||
* @throws WireException
|
||||
*
|
||||
@@ -337,12 +388,14 @@ class Fields extends WireSaveableItems {
|
||||
// even if only the case has changed.
|
||||
$schema = $item->type->getDatabaseSchema($item);
|
||||
if(!empty($schema)) {
|
||||
foreach(array($table, "tmp_$table") as $t) {
|
||||
list(,$tmpTable) = explode(Field::tablePrefix, $table, 2);
|
||||
$tmpTable = "tempf_$tmpTable";
|
||||
foreach(array($table, $tmpTable) as $t) {
|
||||
if(!$database->tableExists($t)) continue;
|
||||
throw new WireException("Cannot rename to '$item->name' because table `$table` already exists");
|
||||
}
|
||||
$database->exec("RENAME TABLE `$prevTable` TO `tmp_$table`"); // QA
|
||||
$database->exec("RENAME TABLE `tmp_$table` TO `$table`"); // QA
|
||||
$database->exec("RENAME TABLE `$prevTable` TO `$tmpTable`"); // QA
|
||||
$database->exec("RENAME TABLE `$tmpTable` TO `$table`"); // QA
|
||||
}
|
||||
$item->prevTable = '';
|
||||
}
|
||||
@@ -432,7 +485,7 @@ class Fields extends WireSaveableItems {
|
||||
*
|
||||
* This method will throw a WireException if you attempt to delete a field that is currently in use (i.e. assigned to one or more fieldgroups).
|
||||
*
|
||||
* @param Field|Saveable $item Field to delete
|
||||
* @param Field $item Field to delete
|
||||
* @return bool True on success, false on failure
|
||||
* @throws WireException
|
||||
*
|
||||
@@ -473,7 +526,7 @@ class Fields extends WireSaveableItems {
|
||||
/**
|
||||
* Create and return a cloned copy of the given Field
|
||||
*
|
||||
* @param Field|Saveable $item Field to clone
|
||||
* @param Field $item Field to clone
|
||||
* @param string $name Optionally specify name for new cloned item
|
||||
* @return Field $item Returns the new clone on success, or false on failure
|
||||
*
|
||||
@@ -658,6 +711,7 @@ class Fields extends WireSaveableItems {
|
||||
$field2->flags = 0; // intentional overwrite after above line
|
||||
}
|
||||
$field2->name = $field2->name . "_PWTMP";
|
||||
$field2->prevFieldtype = $field1->type;
|
||||
$field2->type->createField($field2);
|
||||
$field1->type = $field1->prevFieldtype;
|
||||
|
||||
@@ -1092,6 +1146,30 @@ class Fields extends WireSaveableItems {
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find fields by flag
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param int $flag
|
||||
* @param bool $getFieldNames
|
||||
* @return array|Field[]
|
||||
* @since 3.0.243
|
||||
*
|
||||
*/
|
||||
public function findByFlag($flag, $getFieldNames = false) {
|
||||
if(!isset($this->flagsToIds[$flag])) return array();
|
||||
$items = [];
|
||||
foreach($this->flagsToIds[$flag] as $id) {
|
||||
if($getFieldNames) {
|
||||
$items[] = $this->fieldIdToProperty($id, 'name');
|
||||
} else {
|
||||
$items[] = $this->get($id);
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find fields by type
|
||||
*
|
||||
@@ -1238,7 +1316,7 @@ class Fields extends WireSaveableItems {
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param Field|int|string Field to check
|
||||
* @param Field Field to check
|
||||
* @param string $permission Specify either 'view' or 'edit'
|
||||
* @param Page|null $page Optionally specify a page for context
|
||||
* @param User|null $user Optionally specify a user for context (default=current user)
|
||||
@@ -1246,7 +1324,7 @@ class Fields extends WireSaveableItems {
|
||||
* @throws WireException if given invalid arguments
|
||||
*
|
||||
*/
|
||||
public function _hasPermission(Field $field, $permission, Page $page = null, User $user = null) {
|
||||
public function _hasPermission(Field $field, $permission, ?Page $page = null, ?User $user = null) {
|
||||
if($permission != 'edit' && $permission != 'view') {
|
||||
throw new WireException('Specify either "edit" or "view"');
|
||||
}
|
||||
@@ -1284,7 +1362,7 @@ class Fields extends WireSaveableItems {
|
||||
*
|
||||
* #pw-hooker
|
||||
*
|
||||
* @param Field|Saveable $item
|
||||
* @param Field $item
|
||||
* @param Fieldtype $fromType
|
||||
* @param Fieldtype $toType
|
||||
*
|
||||
@@ -1296,7 +1374,7 @@ class Fields extends WireSaveableItems {
|
||||
*
|
||||
* #pw-hooker
|
||||
*
|
||||
* @param Field|Saveable $item
|
||||
* @param Field $item
|
||||
* @param Fieldtype $fromType
|
||||
* @param Fieldtype $toType
|
||||
*
|
||||
|
@@ -766,7 +766,7 @@ abstract class Fieldtype extends WireData implements Module {
|
||||
$database = $this->wire()->database;
|
||||
|
||||
if(!$database->isOperator($operator)) {
|
||||
throw new WireException("Operator '$operator' is not implemented in $this->className");
|
||||
throw new PageFinderSyntaxException("Operator '$operator' is not implemented in $this->className");
|
||||
}
|
||||
|
||||
$table = $database->escapeTable($table);
|
||||
@@ -1378,12 +1378,13 @@ abstract class Fieldtype extends WireData implements Module {
|
||||
$result = $query->execute();
|
||||
|
||||
} catch(\PDOException $e) {
|
||||
if($e->getCode() == 23000) {
|
||||
$code = (int) $e->getCode();
|
||||
if($code === 23000) {
|
||||
$message = sprintf(
|
||||
$this->_('Value not allowed for field “%s” because it is already in use'),
|
||||
$field->name
|
||||
);
|
||||
throw new WireDatabaseException($message, $e->getCode(), $e);
|
||||
throw new WireDatabaseException($message, $code, $e);
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
@@ -1676,4 +1677,3 @@ abstract class Fieldtype extends WireData implements Module {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@@ -251,7 +251,7 @@ abstract class FieldtypeMulti extends Fieldtype {
|
||||
} catch(\Exception $e) {
|
||||
if($useTransaction) $database->rollBack();
|
||||
if($config->allowExceptions) throw $e; // throw original
|
||||
throw new WireDatabaseQueryException($e->getMessage(), $e->getCode(), $e);
|
||||
WireException([ 'class' => 'WireDatabaseQueryException', 'previous' => $e ]);
|
||||
}
|
||||
|
||||
if(!count($values)) {
|
||||
@@ -343,7 +343,7 @@ abstract class FieldtypeMulti extends Fieldtype {
|
||||
/** @var \PDOException $exception */
|
||||
if($useTransaction) $database->rollBack();
|
||||
if($config->allowExceptions) throw $exception; // throw original
|
||||
throw new WireDatabaseQueryException($exception->getMessage(), $exception->getCode(), $exception);
|
||||
WireException([ 'class' => 'WireDatabaseQueryException', 'previous' => $exception ]);
|
||||
} else {
|
||||
if($useTransaction) $database->commit();
|
||||
}
|
||||
@@ -503,7 +503,7 @@ abstract class FieldtypeMulti extends Fieldtype {
|
||||
|
||||
} else if($col === 'limit') {
|
||||
$value = (int) $value;
|
||||
if($value > 0) $limit = $value;
|
||||
if($value > -1) $limit = $value;
|
||||
|
||||
} else if($col === 'start') {
|
||||
$value = (int) $value;
|
||||
@@ -866,18 +866,24 @@ abstract class FieldtypeMulti extends Fieldtype {
|
||||
*
|
||||
*/
|
||||
public function getLoadQueryAutojoin(Field $field, DatabaseQuerySelect $query) {
|
||||
$database = $this->wire()->database;
|
||||
if($this->get('useOrderByCols')) {
|
||||
// autojoin is not used if sorting or pagination is active
|
||||
$orderByCols = $field->get('orderByCols');
|
||||
if(count($orderByCols) > 0) return null;
|
||||
}
|
||||
$table = $this->database->escapeTable($field->table);
|
||||
$schema = $this->trimDatabaseSchema($this->getDatabaseSchema($field));
|
||||
$fieldName = $this->database->escapeCol($field->name);
|
||||
$table = $database->escapeTable($field->table);
|
||||
$schemaAll = $this->getDatabaseSchema($field);
|
||||
$schema = $this->trimDatabaseSchema($schemaAll);
|
||||
$fieldName = $database->escapeCol($field->name);
|
||||
$separator = self::multiValueSeparator;
|
||||
if($field->distinctAutojoin) $table = "DISTINCT $table";
|
||||
$orderBy = '';
|
||||
if($field->distinctAutojoin) {
|
||||
if(isset($schemaAll['sort'])) $orderBy = "ORDER BY $table.sort";
|
||||
$table = "DISTINCT $table";
|
||||
}
|
||||
foreach($schema as $key => $unused) {
|
||||
$query->select("GROUP_CONCAT($table.$key SEPARATOR '$separator') AS `{$fieldName}__$key`"); // QA
|
||||
$query->select("GROUP_CONCAT($table.$key $orderBy SEPARATOR '$separator') AS `{$fieldName}__$key`"); // QA
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
@@ -1039,5 +1045,3 @@ abstract class FieldtypeMulti extends Fieldtype {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@@ -6,7 +6,7 @@
|
||||
* #pw-summary Maintains a collection of Fieldtype modules.
|
||||
* #pw-var $fieldtypes
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @property FieldtypeCheckbox $FieldtypeCheckbox
|
||||
@@ -88,13 +88,24 @@ class Fieldtypes extends WireArray {
|
||||
*/
|
||||
protected $isAPI = false;
|
||||
|
||||
/**
|
||||
* Construct
|
||||
*
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->usesNumericKeys = false;
|
||||
$this->indexedByName = true;
|
||||
$this->nameProperty = 'className';
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the $fieldtypes API var (load all Fieldtype modules into it)
|
||||
*
|
||||
*/
|
||||
public function init() {
|
||||
$this->isAPI = true;
|
||||
foreach($this->wire()->modules->findByPrefix('Fieldtype', 3) as $name => $module) {
|
||||
foreach($this->wire()->modules->findByPrefix('Fieldtype', 3) as /* $name => */ $module) {
|
||||
$this->add($module);
|
||||
}
|
||||
}
|
||||
@@ -153,16 +164,6 @@ class Fieldtypes extends WireArray {
|
||||
return $item->className();
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this WireArray use numeric keys only?
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
protected function usesNumericKeys() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per the WireArray interface, return a blank copy
|
||||
*
|
||||
@@ -229,5 +230,3 @@ class Fieldtypes extends WireArray {
|
||||
public function getNext($item, $strict = true) { $this->preload(); return parent::getNext($item, $strict); }
|
||||
public function getPrev($item, $strict = true) { $this->preload(); return parent::getPrev($item, $strict); }
|
||||
}
|
||||
|
||||
|
||||
|
@@ -187,6 +187,7 @@ class FileCompiler extends Wire {
|
||||
}
|
||||
|
||||
$this->addExclusion($config->paths->wire);
|
||||
$this->addExclusion($config->paths->root . 'vendor/');
|
||||
|
||||
$rootPath = $config->paths->root;
|
||||
$targetPath = $this->cachePath;
|
||||
@@ -1202,4 +1203,3 @@ class FileCompiler extends Wire {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -37,12 +37,12 @@ function wire($name = 'wire') {
|
||||
*
|
||||
* #pw-group-common
|
||||
*
|
||||
* @param ProcessWire|Wire|null $wire To set specify ProcessWire instance or any Wire-derived object in it, or omit to get current instance.
|
||||
* @param Wire|null $wire To set specify ProcessWire instance or any Wire-derived object in it, or omit to get current instance.
|
||||
* @return ProcessWire
|
||||
* @since 3.0.125
|
||||
*
|
||||
*/
|
||||
function wireInstance(Wire $wire = null) {
|
||||
function wireInstance(?Wire $wire = null) {
|
||||
if($wire === null) return ProcessWire::getCurrentInstance();
|
||||
if(!$wire instanceof ProcessWire) $wire = $wire->wire();
|
||||
ProcessWire::setCurrentInstance($wire);
|
||||
@@ -592,39 +592,90 @@ function wireDate($format = '', $ts = null) {
|
||||
|
||||
/**
|
||||
* Render markup for a system icon
|
||||
*
|
||||
*
|
||||
* It is NOT necessary to specify an icon prefix like “fa-” with the icon name.
|
||||
*
|
||||
* Modifiers recognized in the class attribute:
|
||||
* lg, fw, 2x, 3x, 4x, 5x, spin, spinner, li, border, inverse,
|
||||
* rotate-90, rotate-180, rotate-270, flip-horizontal, flip-vertical,
|
||||
* stack, stack-1x, stack-2x
|
||||
*
|
||||
* ~~~~~
|
||||
* // Outputs: "<i class='fa fa-home'></i>"
|
||||
* echo wireIconMarkup('home');
|
||||
* echo wireIconMarkup('home');
|
||||
*
|
||||
* // Outputs: "<i class='fa fa-home fa-fw fa-lg my-class'></i>"
|
||||
* echo wireIconMarkup('home', 'fw lg my-class');
|
||||
*
|
||||
* // Outputs "<i class='fa fa-home fa-fw' id='root-icon'></i>" (3.0.229+ only)
|
||||
* echo wireIconMarkup('home', 'fw id=root-icon');
|
||||
* echo wireIconMarkup('home fw id=root-icon'); // same as above
|
||||
* ~~~~~
|
||||
*
|
||||
* #pw-group-markup
|
||||
*
|
||||
* @param string $icon Icon name (currently a font-awesome icon name, but support for more in future)
|
||||
* @param string $class Additional attributes for class (example: "fw" for fixed width)
|
||||
* @param string $icon Icon name (currently a font-awesome icon name)
|
||||
* @param string $class Any of the following:
|
||||
* - Additional attributes for class (example: "fw" for fixed width)
|
||||
* - Your own custom class(es) separated by spaces
|
||||
* - Any additional attributes in format `key="val" key='val' or key=val` string (3.0.229+)
|
||||
* - An optional trailing space to append an ` ` to the return icon markup (3.0.229+)
|
||||
* - Any of the above may also be specified in the $icon argument in 3.0.229+.
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
function wireIconMarkup($icon, $class = '') {
|
||||
static $modifiers = null;
|
||||
$sanitizer = wire()->sanitizer;
|
||||
$attrs = array();
|
||||
$append = '';
|
||||
if($modifiers === null) $modifiers = array_flip(array(
|
||||
'lg', 'fw', '2x', '3x', '4x', '5x',
|
||||
'spin', 'spinner', 'li', 'border', 'inverse',
|
||||
'rotate-90', 'rotate-180', 'rotate-270',
|
||||
'flip-horizontal', 'flip-vertical',
|
||||
'stack', 'stack-1x', 'stack-2x',
|
||||
));
|
||||
if(empty($icon)) return '';
|
||||
if(strpos($icon, 'icon-') === 0) $icon = str_replace('icon-', 'fa-', $icon);
|
||||
if(strpos($icon, 'fa-') !== 0) $icon = "fa-$icon";
|
||||
if($class) {
|
||||
$modifiers = array(
|
||||
'lg', 'fw', '2x', '3x', '4x', '5x', 'spin', 'spinner', 'li', 'border',
|
||||
'rotate-90', 'rotate-180', 'rotate-270', 'flip-horizontal', 'flip-vertical',
|
||||
'stack', 'stack-1x', 'stack-2x', 'inverse',
|
||||
);
|
||||
$classes = explode(' ', $class);
|
||||
foreach($classes as $key => $modifier) {
|
||||
if(in_array($modifier, $modifiers)) $classes[$key] = "fa-$modifier";
|
||||
if(strpos($icon, ' ')) {
|
||||
// class or extras specified in $icon rather than $class
|
||||
list($icon, $extra) = explode(' ', $icon, 2);
|
||||
$class = trim("$class $extra");
|
||||
}
|
||||
if(strpos($icon, 'icon-') === 0) {
|
||||
list(,$icon) = explode('icon-', $icon, 2);
|
||||
$icon = "fa-$icon";
|
||||
} else if(strpos($icon, 'fa-') !== 0) {
|
||||
$icon = "fa-$icon";
|
||||
}
|
||||
if($class !== '') {
|
||||
$classes = array();
|
||||
if(rtrim($class) !== $class) $append = ' ';
|
||||
if(strpos($class, '=')) {
|
||||
$re = '/\b([-_a-z\d]+)=("[^"]*"|\'[^\']*\'|[-_a-z\d]+)\s*/i';
|
||||
if(preg_match_all($re, $class, $matches)) {
|
||||
foreach($matches[1] as $key => $attrName) {
|
||||
$attrVal = trim($matches[2][$key], "\"'");
|
||||
$attrVal = $sanitizer->entities($attrVal);
|
||||
$attrs[$attrName] = "$attrName='$attrVal'";
|
||||
$class = str_replace($matches[0][$key], ' ', $class);
|
||||
}
|
||||
$class = trim($class);
|
||||
}
|
||||
}
|
||||
if(isset($attrs['class'])) {
|
||||
$class = trim("$class $attrs[class]");
|
||||
unset($attrs['class']);
|
||||
}
|
||||
foreach(explode(' ', $class) as $c) {
|
||||
if(empty($c)) continue;
|
||||
$classes[] = isset($modifiers[$c]) ? "fa-$c" : $c;
|
||||
}
|
||||
$class = implode(' ', $classes);
|
||||
}
|
||||
$class = trim("fa $icon $class");
|
||||
return "<i class='$class'></i>";
|
||||
$class = $sanitizer->entities(trim("fa $icon $class"));
|
||||
$attrs['class'] = "class='$class'";
|
||||
return "<i " . implode(' ', $attrs) . "></i>$append";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1392,5 +1443,3 @@ function PageArray($items = array()) {
|
||||
$pa = PageArray::newInstance($items);
|
||||
return $pa;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -95,6 +95,7 @@ class ImageSizer extends Wire {
|
||||
*
|
||||
*/
|
||||
public function __construct($filename = '', $options = array()) {
|
||||
parent::__construct();
|
||||
if(!empty($options)) $this->setOptions($options);
|
||||
if(!empty($filename)) $this->setFilename($filename);
|
||||
}
|
||||
@@ -114,7 +115,7 @@ class ImageSizer extends Wire {
|
||||
|
||||
self::$knownEngines = array();
|
||||
|
||||
$modules = $this->wire('modules');
|
||||
$modules = $this->wire()->modules;
|
||||
$engines = $modules->findByPrefix('ImageSizerEngine');
|
||||
$numEngines = count($engines);
|
||||
|
||||
@@ -228,7 +229,6 @@ class ImageSizer extends Wire {
|
||||
$e = $this->getEngine($engineName);
|
||||
if(!$e) continue;
|
||||
|
||||
/** @var ImageSizerEngine $e */
|
||||
$e->prepare($filename, $options, $inspectionResult);
|
||||
$supported = $e->supported();
|
||||
|
||||
@@ -430,7 +430,7 @@ class ImageSizer extends Wire {
|
||||
$engineClass = __NAMESPACE__ . "\\$engineName";
|
||||
$engine = $this->wire(new $engineClass());
|
||||
} else {
|
||||
$engine = $this->wire('modules')->get($engineName);
|
||||
$engine = $this->wire()->modules->get($engineName);
|
||||
}
|
||||
return $engine;
|
||||
}
|
||||
@@ -452,7 +452,7 @@ class ImageSizer extends Wire {
|
||||
return $this->engine;
|
||||
}
|
||||
|
||||
public function __get($key) { return $this->getEngine()->__get($key); }
|
||||
public function __get($name) { return $this->getEngine()->__get($name); }
|
||||
|
||||
/**
|
||||
* ImageInformation from Image Inspector in short form or full RawInfoData
|
||||
@@ -607,7 +607,7 @@ class ImageSizer extends Wire {
|
||||
$count = 0;
|
||||
while(!feof($fh) && $count < 2) {
|
||||
$chunk = fread($fh, 1024 * 100); //read 100kb at a time
|
||||
$count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', $chunk, $matches);
|
||||
$count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', $chunk);
|
||||
}
|
||||
fclose($fh);
|
||||
return $count > 1;
|
||||
@@ -618,7 +618,7 @@ class ImageSizer extends Wire {
|
||||
*
|
||||
* @param mixed $image Pageimage or filename
|
||||
*
|
||||
* @return mixed|null|bool
|
||||
* @return null|bool
|
||||
*
|
||||
*/
|
||||
static public function imageResetIPTC($image) {
|
||||
|
@@ -399,7 +399,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
|
||||
$this->inspectionResult = $inspectionResult;
|
||||
|
||||
// filling all options with global custom values from config.php
|
||||
$options = array_merge($this->wire('config')->imageSizerOptions, $options);
|
||||
$options = array_merge($this->wire()->config->imageSizerOptions, $options);
|
||||
$this->setOptions($options);
|
||||
$this->loadImageInfo($filename, false);
|
||||
}
|
||||
@@ -521,7 +521,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
|
||||
if(is_callable("$className::getModuleInfo")) {
|
||||
$moduleInfo = $className::getModuleInfo();
|
||||
} else {
|
||||
$moduleInfo = $this->wire('modules')->getModuleInfoVerbose($className);
|
||||
$moduleInfo = $this->wire()->modules->getModuleInfoVerbose($className);
|
||||
}
|
||||
|
||||
if(!is_array($moduleInfo)) $moduleInfo = array();
|
||||
@@ -635,7 +635,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
|
||||
*
|
||||
*/
|
||||
public function writeBackIPTC($filename, $includeCustomTags = false) {
|
||||
if($this->wire('config')->debug) {
|
||||
if($this->wire()->config->debug) {
|
||||
// add a timestamp and the name of the image sizer engine to the IPTC tag number 217
|
||||
$entry = $this->className() . '-' . date('Ymd:His');
|
||||
if(!$this->iptcRaw) $this->iptcRaw = array();
|
||||
@@ -648,9 +648,10 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
|
||||
$dest = preg_replace('/\.' . $extension . '$/', '_tmp.' . $extension, $filename);
|
||||
if(strlen($content) == @file_put_contents($dest, $content, \LOCK_EX)) {
|
||||
// on success we replace the file
|
||||
$this->wire('files')->unlink($filename);
|
||||
$this->wire('files')->rename($dest, $filename);
|
||||
$this->wire('files')->chmod($filename);
|
||||
$files = $this->wire()->files;
|
||||
$files->unlink($filename);
|
||||
$files->rename($dest, $filename);
|
||||
$files->chmod($filename);
|
||||
return true;
|
||||
} else {
|
||||
// it was created a temp diskfile but not with all data in it
|
||||
@@ -1460,7 +1461,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
|
||||
/**
|
||||
* Return the image type constant
|
||||
*
|
||||
* @return string
|
||||
* @return string|null
|
||||
*
|
||||
*/
|
||||
public function getImageType() {
|
||||
@@ -1730,15 +1731,17 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
|
||||
$this->finalWidth, $this->finalHeight)) {
|
||||
return false; // fallback or failed
|
||||
}
|
||||
|
||||
$files = $this->wire()->files;
|
||||
|
||||
if($this->webpOnly) {
|
||||
$this->wire('files')->unlink($this->tmpFile);
|
||||
$files->unlink($this->tmpFile);
|
||||
} else {
|
||||
// all went well, copy back the temp file,
|
||||
if(!@copy($this->tmpFile, $this->filename)) return false; // fallback or failed
|
||||
$this->wire('files')->chmod($this->filename);
|
||||
$files->chmod($this->filename);
|
||||
// remove the temp file
|
||||
$this->wire('files')->unlink($this->tmpFile);
|
||||
$files->unlink($this->tmpFile);
|
||||
// post processing: IPTC, setModified and reload ImageInfo
|
||||
$this->writeBackIPTC($this->filename, false);
|
||||
}
|
||||
@@ -1758,6 +1761,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
|
||||
*/
|
||||
public function rotate($degrees, $dstFilename = '') {
|
||||
|
||||
$files = $this->wire()->files;
|
||||
$degrees = (int) $degrees;
|
||||
$srcFilename = $this->filename;
|
||||
|
||||
@@ -1767,7 +1771,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
|
||||
if($degrees < -360) $degrees = $degrees - 360;
|
||||
|
||||
if($degrees == 0 || $degrees == 360 || $degrees == -360) {
|
||||
if($dstFilename != $this->filename) wireCopy($this->filename, $dstFilename);
|
||||
if($dstFilename != $this->filename) $files->copy($this->filename, $dstFilename);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1787,13 +1791,13 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
|
||||
if($result) {
|
||||
// success
|
||||
if($tmpFilename != $dstFilename) {
|
||||
if(is_file($dstFilename)) $this->wire('files')->unlink($dstFilename);
|
||||
$this->wire('files')->rename($tmpFilename, $dstFilename);
|
||||
if(is_file($dstFilename)) $files->unlink($dstFilename);
|
||||
$files->rename($tmpFilename, $dstFilename);
|
||||
}
|
||||
$this->wire('files')->chmod($dstFilename);
|
||||
$files->chmod($dstFilename);
|
||||
} else {
|
||||
// fail
|
||||
if(is_file($tmpFilename)) $this->wire('files')->unlink($tmpFilename);
|
||||
if(is_file($tmpFilename)) $files->unlink($tmpFilename);
|
||||
}
|
||||
|
||||
return $result;
|
||||
@@ -2076,7 +2080,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
|
||||
*/
|
||||
public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {
|
||||
|
||||
$f = $this->wire('modules')->get('InputfieldInteger');
|
||||
$f = $inputfields->InputfieldInteger;
|
||||
$f->attr('name', 'enginePriority');
|
||||
$f->label = $this->_('Engine priority');
|
||||
$f->description = $this->_('This determines what order this engine is tried in relation to other ImageSizerEngine modules.');
|
||||
@@ -2086,7 +2090,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
|
||||
$f->icon = 'sort-numeric-asc';
|
||||
$inputfields->add($f);
|
||||
|
||||
$f = $this->wire('modules')->get('InputfieldRadios');
|
||||
$f = $inputfields->InputfieldRadios;
|
||||
$f->attr('name', 'sharpening');
|
||||
$f->label = $this->_('Sharpening');
|
||||
$f->addOption('none', $this->_('None'));
|
||||
@@ -2098,7 +2102,7 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
|
||||
$f->icon = 'image';
|
||||
$inputfields->add($f);
|
||||
|
||||
$f = $this->wire('modules')->get('InputfieldInteger');
|
||||
$f = $inputfields->InputfieldInteger;
|
||||
$f->attr('name', 'quality');
|
||||
$f->label = $this->_('Quality');
|
||||
$f->description = $this->_('Default quality setting from 1 to 100 where 1 is lowest quality, and 100 is highest.');
|
||||
|
@@ -377,7 +377,7 @@ class ImageSizerEngineGD extends ImageSizerEngine {
|
||||
}
|
||||
|
||||
// write to file(s)
|
||||
if(file_exists($dstFilename)) $this->wire('files')->unlink($dstFilename);
|
||||
if(file_exists($dstFilename)) $this->wire()->files->unlink($dstFilename);
|
||||
|
||||
$result = null; // null=not yet known
|
||||
|
||||
@@ -457,7 +457,7 @@ class ImageSizerEngineGD extends ImageSizerEngine {
|
||||
if(!function_exists('imagewebp')) return false;
|
||||
$path_parts = pathinfo($filename);
|
||||
$webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp';
|
||||
if(file_exists($webpFilename)) $this->wire('files')->unlink($webpFilename);
|
||||
if(file_exists($webpFilename)) $this->wire()->files->unlink($webpFilename);
|
||||
return imagewebp($im, $webpFilename, $quality);
|
||||
}
|
||||
|
||||
|
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* ProcessWire Inputfield - base class for Inputfield modules.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* An Inputfield for an actual form input field widget, and this is provided as the base class
|
||||
@@ -62,6 +62,7 @@
|
||||
* @property string $tabLabel Label for tab if Inputfield rendered in its own tab via Inputfield::collapsedTab* setting. @since 3.0.201 #pw-group-labels
|
||||
* @property string|null $prependMarkup Optional markup to prepend to the Inputfield content container. #pw-group-other
|
||||
* @property string|null $appendMarkup Optional markup to append to the Inputfield content container. #pw-group-other
|
||||
* @property string|null $footerMarkup Optional markup to add to the '.Inputfield' container, after '.InputfieldContent'. @since 3.0.241 #pw-advanced
|
||||
*
|
||||
* @method string|Inputfield label($label = null) Get or set the 'label' property via method. @since 3.0.110 #pw-group-labels
|
||||
* @method string|Inputfield description($description = null) Get or set the 'description' property via method. @since 3.0.110 #pw-group-labels
|
||||
@@ -102,16 +103,16 @@
|
||||
* @property null|bool|Fieldtype $hasFieldtype The Fieldtype using this Inputfield, or boolean false when known not to have a Fieldtype, or null when not known. #pw-group-other
|
||||
* @property null|Field $hasField The Field object associated with this Inputfield, or null when not applicable or not known. #pw-group-other
|
||||
* @property null|Page $hasPage The Page object associated with this Inputfield, or null when not applicable or not known. #pw-group-other
|
||||
* @property null|Inputfield $hasInputfield If this Inputfield is owned/managed by another (other than parent/child relationship), it may be set here. 3.0.176+ #pw-group-other
|
||||
* @property bool|null $useLanguages When multi-language support active, can be set to true to make it provide inputs for each language, where supported (default=false). #pw-group-behavior
|
||||
* @property null|Inputfield $hasInputfield If this Inputfield is owned/managed by another (other than parent/child relationship), it may be set here. @since 3.0.176 #pw-group-other
|
||||
* @property bool|null $useLanguages When multi-language support active, can be set to true to make it provide inputs for each language, where supported (default=false). #pw-group-behavior #pw-group-languages
|
||||
* @property null|bool|int $entityEncodeLabel Set to boolean false to specifically disable entity encoding of field header/label (default=true). #pw-group-output
|
||||
* @property null|bool $entityEncodeText Set to boolean false to specifically disable entity encoding for other text: description, notes, etc. (default=true). #pw-group-output
|
||||
* @property int $renderFlags Options that can be applied to render, see "render*" constants (default=0). #pw-group-output 3.0.204+
|
||||
* @property int $renderFlags Options that can be applied to render, see "render*" constants (default=0). @since 3.0.204 #pw-group-output
|
||||
* @property int $renderValueFlags Options that can be applied to renderValue mode, see "renderValue" constants (default=0). #pw-group-output
|
||||
* @property string $wrapClass Optional class name (CSS) to apply to the HTML element wrapping the Inputfield. #pw-group-other
|
||||
* @property string $headerClass Optional class name (CSS) to apply to the InputfieldHeader element #pw-group-other
|
||||
* @property string $contentClass Optional class name (CSS) to apply to the InputfieldContent element #pw-group-other
|
||||
* @property string $addClass Formatted class string letting you add class to any of the above (see addClass method). #pw-group-other 3.0.204+
|
||||
* @property string $addClass Formatted class string letting you add class to any of the above (see addClass method). @since 3.0.204 #pw-group-other
|
||||
* @property int|null $textFormat Text format to use for description/notes text in Inputfield (see textFormat constants) #pw-group-output
|
||||
*
|
||||
* @method string|Inputfield required($required = null) Get or set required state. @since 3.0.110 #pw-group-behavior
|
||||
@@ -121,7 +122,11 @@
|
||||
* @method string|Inputfield headerClass($class = null) Get header class attribute or add a class to it. @since 3.0.110 #pw-group-other
|
||||
* @method string|Inputfield contentClass($class = null) Get content class attribute or add a class to it. @since 3.0.110 #pw-group-other
|
||||
*
|
||||
*
|
||||
* MULTI-LANGUAGE METHODS (requires LanguageSupport module to be installed)
|
||||
* ======================
|
||||
* @method void setLanguageValue($language, $value) Set language value for Inputfield that supports it. Requires LanguageSupport module. $language can be Language, id (int) or name (string). @since 3.0.238 #pw-group-languages
|
||||
* @method string|mixed getLanguageValue($language) Get language value for Inputfield that supports it. Requires LanguageSupport module. $language can be Language, id (int) or name (string). @since 3.0.238 #pw-group-languages
|
||||
*
|
||||
* HOOKABLE METHODS
|
||||
* ================
|
||||
* @method string render()
|
||||
@@ -393,6 +398,14 @@ abstract class Inputfield extends WireData implements Module {
|
||||
*/
|
||||
protected $editable = true;
|
||||
|
||||
/**
|
||||
* Header icon definitions
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
*/
|
||||
protected $headerActions = array();
|
||||
|
||||
/**
|
||||
* Construct the Inputfield, setting defaults for all properties
|
||||
*
|
||||
@@ -421,8 +434,9 @@ abstract class Inputfield extends WireData implements Module {
|
||||
$this->set('textFormat', self::textFormatBasic); // format applied to description and notes
|
||||
$this->set('renderFlags', 0); // See render* constants
|
||||
$this->set('renderValueFlags', 0); // see renderValue* constants, applicable to renderValue mode only
|
||||
$this->set('prependMarkup', ''); // markup to prepend to Inputfield output
|
||||
$this->set('appendMarkup', ''); // markup to append to Inputfield output
|
||||
$this->set('prependMarkup', ''); // markup to prepend to InputfieldContent output
|
||||
$this->set('appendMarkup', ''); // markup to append to InputfieldContent output
|
||||
$this->set('footerMarkup', ''); // markup to add to end of Inputfield output
|
||||
|
||||
// default ID attribute if no 'id' attribute set
|
||||
$this->defaultID = $this->className() . self::$numInstances;
|
||||
@@ -505,7 +519,7 @@ abstract class Inputfield extends WireData implements Module {
|
||||
if($value === true) $value = self::collapsedYes;
|
||||
$value = (int) $value;
|
||||
|
||||
} else if(array_key_exists($key, $this->attributes)) {
|
||||
} else if(array_key_exists($key, $this->attributes) && $key !== 'required') {
|
||||
return $this->setAttribute($key, $value);
|
||||
|
||||
} else if($key === 'required' && $value && !is_object($value)) {
|
||||
@@ -936,7 +950,7 @@ abstract class Inputfield extends WireData implements Module {
|
||||
*/
|
||||
protected function ___callUnknown($method, $arguments) {
|
||||
$arg = isset($arguments[0]) ? $arguments[0] : null;
|
||||
if(isset($this->attributes[$method])) {
|
||||
if(isset($this->attributes[$method]) && $method !== 'required') {
|
||||
// get or set an attribute
|
||||
return $arg === null ? $this->getAttribute($method) : $this->setAttribute($method, $arg);
|
||||
} else if(($value = $this->getSetting($method)) !== null) {
|
||||
@@ -1356,11 +1370,11 @@ abstract class Inputfield extends WireData implements Module {
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param array $attributes Associative array of attributes to build the string from, or omit to use this Inputfield's attributes.
|
||||
* @param array|null $attributes Associative array of attributes to build the string from, or omit to use this Inputfield's attributes.
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public function getAttributesString(array $attributes = null) {
|
||||
public function getAttributesString(?array $attributes = null) {
|
||||
|
||||
$str = '';
|
||||
|
||||
@@ -1392,7 +1406,7 @@ abstract class Inputfield extends WireData implements Module {
|
||||
continue;
|
||||
}
|
||||
|
||||
$str .= "$attr=\"" . htmlspecialchars($value, ENT_QUOTES, "UTF-8") . '" ';
|
||||
$str .= "$attr=\"" . htmlspecialchars("$value", ENT_QUOTES, "UTF-8") . '" ';
|
||||
}
|
||||
|
||||
return trim($str);
|
||||
@@ -1449,13 +1463,17 @@ abstract class Inputfield extends WireData implements Module {
|
||||
*
|
||||
* #pw-group-output
|
||||
*
|
||||
* @param Inputfield|InputfieldWrapper|null The parent InputfieldWrapper that is rendering it, or null if no parent.
|
||||
* @param Inputfield|null The parent InputfieldWrapper that is rendering it, or null if no parent.
|
||||
* @param bool $renderValueMode Specify true only if this is for `Inputfield::renderValue()` rather than `Inputfield::render()`.
|
||||
* @return bool True if assets were just added, false if already added.
|
||||
*
|
||||
*/
|
||||
public function renderReady(Inputfield $parent = null, $renderValueMode = false) {
|
||||
$result = $this->wire()->modules->loadModuleFileAssets($this) > 0;
|
||||
public function renderReady(?Inputfield $parent = null, $renderValueMode = false) {
|
||||
if($this->className() === 'InputfieldWrapper') {
|
||||
$result = false;
|
||||
} else {
|
||||
$result = $this->wire()->modules->loadModuleFileAssets($this) > 0;
|
||||
}
|
||||
if($this->wire()->hooks->isMethodHooked($this, 'renderReadyHook')) {
|
||||
$this->renderReadyHook($parent, $renderValueMode);
|
||||
}
|
||||
@@ -1467,11 +1485,11 @@ abstract class Inputfield extends WireData implements Module {
|
||||
*
|
||||
* Hook this method instead if you want to hook renderReady().
|
||||
*
|
||||
* @param Inputfield $parent
|
||||
* @param Inputfield|null $parent
|
||||
* @param bool $renderValueMode
|
||||
*
|
||||
*/
|
||||
public function ___renderReadyHook(Inputfield $parent = null, $renderValueMode = false) { }
|
||||
public function ___renderReadyHook(?Inputfield $parent = null, $renderValueMode = false) { }
|
||||
|
||||
/**
|
||||
* This hook was replaced by renderReady
|
||||
@@ -2077,6 +2095,86 @@ abstract class Inputfield extends WireData implements Module {
|
||||
return $this->editable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add header action
|
||||
*
|
||||
* This adds a clickable icon to the right side of the Inputfield header.
|
||||
* There are three types of actions: 'click', 'toggle' and 'link'. The 'click'
|
||||
* action simply triggers your JS event whenever it is clicked. The 'toggle' action
|
||||
* has an on/off state, and you can specify the JS event to trigger for each.
|
||||
* This function will automatically figure out whether you want a `click`,
|
||||
* `toggle` or 'link' action based on what you provide in the $settings argument.
|
||||
* Below is a summary of these settings:
|
||||
*
|
||||
* Settings for 'click' or 'link' type actions
|
||||
* -------------------------------------------
|
||||
* - `icon` (string): Name of font-awesome icon to use.
|
||||
* - `tooltip` (string): Optional tooltip text to display when icon hovered.
|
||||
* - `event` (string): Event name to trigger in JS when clicked ('click' actions only).
|
||||
* - `href` (string): URL to open ('link' actions only).
|
||||
* - `modal` (bool): Specify true to open link in modal ('link' actions only).
|
||||
*
|
||||
* Settings for 'toggle' (on/off) type actions
|
||||
* -------------------------------------------
|
||||
* - `on` (bool): Start with the 'on' state? (default=false)
|
||||
* - `onIcon` (string): Name of font-awesome icon to show for on state.
|
||||
* - `onEvent` (string): JS event name to trigger when toggled on.
|
||||
* - `onTooltip` (string): Tooltip text to show when on icon is hovered.
|
||||
* - `offIcon` (string): Name of font-awesome icon to show for off state.
|
||||
* - `offEvent` (string): JS event name to trigger when toggled off.
|
||||
* - `offTooltip` (string): Tooltip text to show when off icon is hovered.
|
||||
*
|
||||
* Other/optional settings (applies to all types)
|
||||
* ----------------------------------------------
|
||||
* - `name` (string): Name of this action (-_a-zA-Z0-9).
|
||||
* - `parent` (string): Name of parent action, if this action is part of a menu.
|
||||
* - `overIcon` (string): Name of font-awesome icon to show when hovered.
|
||||
* - `overEvent` (string): JS event name to trigger when mouse is over the icon.
|
||||
* - `downIcon` (string): Icon to display when mouse is down on the action icon (3.0.241+).
|
||||
* - `downEvent` (string): JS event name to trigger when mouse is down on the icon (3.0.241+).
|
||||
* - `cursor` (string): CSS cursor name to show when mouse is over the icon.
|
||||
* - `setAll` (array): Set all of the header actions in one call, replaces any existing.
|
||||
* Note: to get all actions, call the method and omit the $settings argument.
|
||||
*
|
||||
* Settings for dropdown menu actions (3.0.241+)
|
||||
* ---------------------------------------------
|
||||
* Note that menu type actions also require jQuery UI and /wire/templates-admin/scripts/main.js,
|
||||
* both of which are already present in PW’s admin themes (AdminThemeUikit recommended).
|
||||
* Requires ProcessWire 3.0.241 or newer.
|
||||
* - `icon` (string): Icon name to use for dropdown toggle, i.e. 'fa-wrench'.
|
||||
* - `tooltip` (string): Optional tooltip to describe what the dropdown is for.
|
||||
* - `menuAction` (string): Action that toggles the menu to show, one of 'click' or 'hover' (default).
|
||||
* - `menuItems` (array): Definition of menu items, each with one or more of the following properties.
|
||||
* - `label` (string): Label text for the menu item (required).
|
||||
* - `icon` (string): Icon name for the menu item, if desired.
|
||||
* - `callback` (function|null): JS callback to execute item is clicked (not applicable in PHP).*
|
||||
* - `event` (string): JS event name to trigger when item is clicked.
|
||||
* - `tooltip` (string): Tooltip text to show when hovering menu item (title attribute).
|
||||
* - `href` (string): URL to go to when menu item clicked.
|
||||
* - `target` (string): Target attribute when href is used (i.e. "_blank").
|
||||
* - `modal` (bool): Open href in modal window instead?
|
||||
* - `active` (function|bool): Callback function that returns true if menu item active, or false.*
|
||||
* if disabled. You can also directly specify true or false for this option.
|
||||
* - NOTE 1: All `menuItems` properties above are optional, except for 'label'.
|
||||
* - NOTE 2: To use `callback` or `active` as functions, you must define your menu in JS instead.
|
||||
* - NOTE 3: For examples see the addHeaderAction() method in /wire/templates-admin/scripts/inputfields.js
|
||||
*
|
||||
* @param array $settings Specify array containing the appropriate settings above.
|
||||
* @return array Returns all currently added actions.
|
||||
* @since 3.0.240
|
||||
*
|
||||
*/
|
||||
public function addHeaderAction(array $settings = array()) {
|
||||
if(!empty($settings['setAll'])) {
|
||||
if(is_array($settings['setAll'])) {
|
||||
$this->headerActions = array_values($settings['setAll']);
|
||||
}
|
||||
} else {
|
||||
$this->headerActions[] = $settings; // add new action
|
||||
}
|
||||
return $this->headerActions; // return all
|
||||
}
|
||||
|
||||
/**
|
||||
* debugInfo PHP 5.6+ magic method
|
||||
*
|
||||
|
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* ProcessWire InputfieldWrapper
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* About InputfieldWrapper
|
||||
@@ -15,6 +15,8 @@
|
||||
*
|
||||
* InputfieldWrapper is not designed to render an Inputfield specifically, but you can set a value attribute
|
||||
* containing content that will be rendered before the wrapper.
|
||||
*
|
||||
* #pw-summary-properties Access any common Inputfield type class name from an InputfieldWrapper and it will return a new instance of that Inputfield, i.e. `$f = $inputfields->InputfieldText;` Below are several examples.
|
||||
*
|
||||
* @property bool $renderValueMode True when only rendering values, i.e. no inputs (default=false). #pw-internal
|
||||
* @property bool $quietMode True to suppress label, description and notes, often combined with renderValueMode (default=false). #pw-internal
|
||||
@@ -27,47 +29,47 @@
|
||||
* @method Inputfield new($typeName, $name = '', $label = '', array $settings = []) #pw-group-manipulation
|
||||
* @method bool allowProcessInput(Inputfield $inputfield) Allow Inputfield to have input processed? (3.0.207+) #pw-internal
|
||||
*
|
||||
* @property InputfieldAsmSelect $InputfieldAsmSelect
|
||||
* @property InputfieldButton $InputfieldButton
|
||||
* @property InputfieldCheckbox $InputfieldCheckbox
|
||||
* @property InputfieldCheckboxes $InputfieldCheckboxes
|
||||
* @property InputfieldCKEditor $InputfieldCkeditor
|
||||
* @property InputfieldCommentsAdmin $InputfieldCommentsAdmin
|
||||
* @property InputfieldDatetime $InputfieldDatetime
|
||||
* @property InputfieldEmail $InputfieldEmail
|
||||
* @property InputfieldFieldset $InputfieldFieldset
|
||||
* @property InputfieldFieldsetClose $InputfieldlFieldsetClose
|
||||
* @property InputfieldFieldsetOpen $InputfieldFieldsetOpen
|
||||
* @property InputfieldFieldsetTabOpen $InputfieldFieldsetTabOpen
|
||||
* @property InputfieldFile $InputfieldFile
|
||||
* @property InputfieldFloat $InputfieldFloat
|
||||
* @property InputfieldForm $InputfieldForm
|
||||
* @property InputfieldHidden $InputfieldHidden
|
||||
* @property InputfieldIcon $InputfieldIcon
|
||||
* @property InputfieldImage $InputfieldImage
|
||||
* @property InputfieldInteger $InputfieldInteger
|
||||
* @property InputfieldMarkup $InputfieldMarkup
|
||||
* @property InputfieldName $InputfieldName
|
||||
* @property InputfieldPage $InputfieldPage
|
||||
* @property InputfieldPageAutocomplete $InputfieldPageAutocomplete
|
||||
* @property InputfieldPageListSelect $InputfieldPageListSelect
|
||||
* @property InputfieldPageListSelectMultiple $InputfieldPageListSelectMultiple
|
||||
* @property InputfieldPageName $InputfieldPageName
|
||||
* @property InputfieldPageTable $InputfieldPageTable
|
||||
* @property InputfieldPageTitle $InputfieldPageTitle
|
||||
* @property InputfieldPassword $InputfieldPassword
|
||||
* @property InputfieldRadios $InputfieldRadios
|
||||
* @property InputfieldRepeater $InputfieldRepeater
|
||||
* @property InputfieldSelect $InputfieldSelect
|
||||
* @property InputfieldSelectMultiple $InputfieldSelectMultiple
|
||||
* @property InputfieldSelector $InputfieldSelector
|
||||
* @property InputfieldSubmit $InputfieldSubmit
|
||||
* @property InputfieldText $InputfieldText
|
||||
* @property InputfieldTextarea $InputfieldTextarea
|
||||
* @property InputfieldTextTags $InputfieldTextTags
|
||||
* @property InputfieldToggle $InputfieldToggle
|
||||
* @property InputfieldURL $InputfieldURL
|
||||
* @property InputfieldWrapper $InputfieldWrapper
|
||||
* @property InputfieldAsmSelect $InputfieldAsmSelect Create new asmSelect Inputfield #pw-group-properties
|
||||
* @property InputfieldButton $InputfieldButton Create new button Inputfield #pw-group-properties
|
||||
* @property InputfieldCheckbox $InputfieldCheckbox Create new checkbox Inputfield #pw-group-properties
|
||||
* @property InputfieldCheckboxes $InputfieldCheckboxes Create new checkboxes Inputfield #pw-group-properties
|
||||
* @property InputfieldCKEditor $InputfieldCKEditor Create new CKEditor Inputfield #pw-group-properties
|
||||
* @property InputfieldCommentsAdmin $InputfieldCommentsAdmin #pw-internal
|
||||
* @property InputfieldDatetime $InputfieldDatetime Create new date/time Inputfield #pw-group-properties
|
||||
* @property InputfieldEmail $InputfieldEmail Create new email Inputfield #pw-group-properties
|
||||
* @property InputfieldFieldset $InputfieldFieldset Create new Fieldset InputfieldWrapper #pw-group-properties
|
||||
* @property InputfieldFieldsetClose $InputfieldlFieldsetClose #pw-internal
|
||||
* @property InputfieldFieldsetOpen $InputfieldFieldsetOpen #pw-internal
|
||||
* @property InputfieldFieldsetTabOpen $InputfieldFieldsetTabOpen #pw-internal
|
||||
* @property InputfieldFile $InputfieldFile Create new file Inputfield #pw-group-properties
|
||||
* @property InputfieldFloat $InputfieldFloat Create new float Inputfield #pw-group-properties
|
||||
* @property InputfieldForm $InputfieldForm Create new form InputfieldWrapper #pw-group-properties
|
||||
* @property InputfieldHidden $InputfieldHidden Create new hidden Inputfield #pw-group-properties
|
||||
* @property InputfieldIcon $InputfieldIcon Create new icon Inputfield #pw-group-properties
|
||||
* @property InputfieldImage $InputfieldImage Create new image Inputfield #pw-group-properties
|
||||
* @property InputfieldInteger $InputfieldInteger Create new integer Inputfield #pw-group-properties
|
||||
* @property InputfieldMarkup $InputfieldMarkup Create new markup Inputfield #pw-group-properties
|
||||
* @property InputfieldName $InputfieldName #pw-internal
|
||||
* @property InputfieldPage $InputfieldPage Create new Page selection Inputfield #pw-group-properties
|
||||
* @property InputfieldPageAutocomplete $InputfieldPageAutocomplete Create new Page selection autocomplete Inputfield #pw-group-properties
|
||||
* @property InputfieldPageListSelect $InputfieldPageListSelect Create new PageListSelect Inputfield #pw-group-properties
|
||||
* @property InputfieldPageListSelectMultiple $InputfieldPageListSelectMultiple Create new multiple PageListSelect Inputfield #pw-group-properties
|
||||
* @property InputfieldPageName $InputfieldPageName #pw-internal
|
||||
* @property InputfieldPageTable $InputfieldPageTable #pw-internal
|
||||
* @property InputfieldPageTitle $InputfieldPageTitle #pw-internal
|
||||
* @property InputfieldPassword $InputfieldPassword #pw-internal
|
||||
* @property InputfieldRadios $InputfieldRadios Create new radio buttons Inputfield #pw-group-properties
|
||||
* @property InputfieldRepeater $InputfieldRepeater #pw-internal
|
||||
* @property InputfieldSelect $InputfieldSelect Create new <select> Inputfield #pw-group-properties
|
||||
* @property InputfieldSelectMultiple $InputfieldSelectMultiple Create new <select multiple> Inputfield #pw-group-properties
|
||||
* @property InputfieldSelector $InputfieldSelector #pw-internal
|
||||
* @property InputfieldSubmit $InputfieldSubmit Create new submit button Inputfield #pw-group-properties
|
||||
* @property InputfieldText $InputfieldText Create new single-line text Inputfield #pw-group-properties
|
||||
* @property InputfieldTextarea $InputfieldTextarea Create new multi-line <textarea> Inputfield #pw-group-properties
|
||||
* @property InputfieldTextTags $InputfieldTextTags Create new text tags Inputfield #pw-group-properties
|
||||
* @property InputfieldToggle $InputfieldToggle Create new toggle Inputfield #pw-group-properties
|
||||
* @property InputfieldURL $InputfieldURL Create new URL Inputfield #pw-group-properties
|
||||
* @property InputfieldWrapper $InputfieldWrapper Create new generic InputfieldWrapper #pw-group-properties
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -89,7 +91,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
|
||||
'list' => "<ul {attrs}>{out}</ul>",
|
||||
'item' => "<li {attrs}>{out}</li>",
|
||||
'item_label' => "<label class='InputfieldHeader ui-widget-header{class}' for='{for}'>{out}</label>",
|
||||
'item_label_hidden' => "<label class='InputfieldHeader InputfieldHeaderHidden ui-widget-header{class}'><span>{out}</span></label>",
|
||||
'item_label_hidden' => "<label class='InputfieldHeader InputfieldHeaderHidden ui-widget-header{class}' for='{for}'><span>{out}</span></label>",
|
||||
'item_content' => "<div class='InputfieldContent ui-widget-content{class}'>{out}</div>",
|
||||
'item_error' => "<p class='InputfieldError ui-state-error'><i class='fa fa-fw fa-flash'></i><span>{out}</span></p>",
|
||||
'item_description' => "<p class='description'>{out}</p>",
|
||||
@@ -402,6 +404,8 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
|
||||
/**
|
||||
* Insert new or existing Inputfield before or after another
|
||||
*
|
||||
* #pw-group-manipulation
|
||||
*
|
||||
* @param Inputfield|array|string $item New or existing item Inputfield, name, or new item array to insert.
|
||||
* @param Inputfield|string $existingItem Existing item or item name you want to insert before.
|
||||
* @param bool $before True to insert before, false to insert after (default=false).
|
||||
@@ -555,6 +559,27 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an Inputfield from the form by name
|
||||
*
|
||||
* Note that this works the same as the getByName/getChildByName methods in that it
|
||||
* will find (and remove) the field by name, even if nested within other wrappers
|
||||
* or fieldsets. It returns the removed Inputfield when found, or null if not.
|
||||
*
|
||||
* @param string $name
|
||||
* @return Inputfield|null Removed Inputfield object on success, or null if not found
|
||||
* @since 3.0.250
|
||||
*
|
||||
*/
|
||||
public function removeByName($name) {
|
||||
$f = $this->getByName((string) $name);
|
||||
if(!$f) return null;
|
||||
$parent = $f->getParent();
|
||||
if(!$parent instanceof InputfieldWrapper) return null;
|
||||
$parent->remove($f);
|
||||
return $f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare children for rendering by creating any fieldset groups
|
||||
*
|
||||
@@ -721,6 +746,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
|
||||
$classes = array();
|
||||
$useColumnWidth = $this->useColumnWidth;
|
||||
$renderAjaxInputfield = $this->wire()->config->ajax ? $this->wire()->input->get('renderInputfieldAjax') : null;
|
||||
$toggleLabel = $sanitizer->entities1($this->_('Toggle open/close'));
|
||||
|
||||
$lockedStates = array(
|
||||
Inputfield::collapsedNoLocked,
|
||||
@@ -766,7 +792,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
|
||||
if(in_array($collapsed, $lockedStates)) $renderValueMode = true;
|
||||
|
||||
$ffOut = $this->renderInputfield($inputfield, $renderValueMode);
|
||||
if(!strlen($ffOut)) continue;
|
||||
if(!strlen("$ffOut")) continue;
|
||||
$collapsed = (int) $inputfield->getSetting('collapsed'); // retrieve again after render
|
||||
$entityEncodeText = $inputfield->getSetting('entityEncodeText') === false ? false : true;
|
||||
|
||||
@@ -883,25 +909,29 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
|
||||
$icon = $icon ? str_replace('{name}', $sanitizer->name(str_replace(array('icon-', 'fa-'), '', $icon)), $markup['item_icon']) : '';
|
||||
$toggle = $collapsed == Inputfield::collapsedNever ? '' : $markup['item_toggle'];
|
||||
if($toggle && strpos($toggle, 'title=') === false) {
|
||||
$toggle = str_replace("class=", "title='" . $this->_('Toggle open/close') . "' class=", $toggle);
|
||||
$toggle = str_replace("class=", "title='$toggleLabel' class=", $toggle);
|
||||
}
|
||||
$headerActions = $inputfield->addHeaderAction();
|
||||
if(count($headerActions)) {
|
||||
$label .= $this->renderHeaderActions($inputfield, $headerActions);
|
||||
}
|
||||
if($skipLabel === Inputfield::skipLabelHeader || $quietMode) {
|
||||
// label only shows when field is collapsed
|
||||
$label = str_replace('{out}', $icon . $label . $toggle, $markup['item_label_hidden']);
|
||||
$labelHidden = $markup['item_label_hidden'];
|
||||
if(strpos($labelHidden, '{for}')) $labelHidden = str_replace('{for}', $inputfield->attr('id'), $labelHidden);
|
||||
$label = str_replace('{out}', $icon . $label . $toggle, $labelHidden);
|
||||
} else {
|
||||
// label always visible
|
||||
$label = str_replace(array('{for}', '{out}'), array($for, $icon . $label . $toggle), $markup['item_label']);
|
||||
$label = str_replace('{out}', $icon . $label . $toggle, $markup['item_label']);
|
||||
if($skipLabel === Inputfield::skipLabelFor) {
|
||||
$label = $this->removeAttributeFromMarkup('for', $label);
|
||||
} else {
|
||||
$label = $this->setAttributeInMarkup('for', $for, $label, true);
|
||||
}
|
||||
}
|
||||
$headerClass = trim($inputfield->getSetting('headerClass') . " $classes[item_label]");
|
||||
if($headerClass) {
|
||||
if(strpos($label, '{class}') !== false) {
|
||||
$label = str_replace('{class}', ' ' . $headerClass, $label);
|
||||
} else {
|
||||
$label = preg_replace('/( class=[\'"][^\'"]+)/', '$1 ' . $headerClass, $label, 1);
|
||||
}
|
||||
} else if(strpos($label, '{class}') !== false) {
|
||||
$label = str_replace('{class}', '', $label);
|
||||
}
|
||||
$label = $this->setAttributeInMarkup('class', $headerClass, $label);
|
||||
|
||||
} else if($skipLabel === Inputfield::skipLabelMarkup) {
|
||||
// no header and no markup for header
|
||||
$label = '';
|
||||
@@ -960,16 +990,9 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
|
||||
}
|
||||
$markupItemContent = $markup['item_content'];
|
||||
$contentClass = trim($inputfield->getSetting('contentClass') . " $classes[item_content]");
|
||||
if($contentClass) {
|
||||
if(strpos($markupItemContent, '{class}') !== false) {
|
||||
$markupItemContent = str_replace('{class}', ' ' . $contentClass, $markupItemContent);
|
||||
} else {
|
||||
$markupItemContent = preg_replace('/( class=[\'"][^\'"]+)/', '$1 ' . $contentClass, $markupItemContent, 1);
|
||||
}
|
||||
} else if(strpos($markupItemContent, '{class}') !== false) {
|
||||
$markupItemContent = str_replace('{class}', '', $markupItemContent);
|
||||
}
|
||||
if($inputfield->className() != 'InputfieldWrapper') $ffOut = str_replace('{out}', $ffOut, $markupItemContent);
|
||||
$markupItemContent = $this->setAttributeInMarkup('class', $contentClass, $markupItemContent);
|
||||
if($inputfield->className() != 'InputfieldWrapper') $ffOut = str_replace('{out}', $ffOut, $markupItemContent);
|
||||
$ffOut .= $inputfield->getSetting('footerMarkup');
|
||||
$out .= str_replace(array('{attrs}', '{out}'), array(trim($attrs), $label . $ffOut), $markup['item']);
|
||||
$lastInputfield = $inputfield;
|
||||
} // foreach($children as $inputfield)
|
||||
@@ -992,6 +1015,129 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set attribute value in markup, optionally replacing a {placeholder} tag
|
||||
*
|
||||
* When a placeholder is present in the given $markup, it should be the
|
||||
* attribute name wrapped in `{}`, i.e. `{class}`
|
||||
*
|
||||
* Note that class attributes are appended while other attributes are replaced.
|
||||
*
|
||||
* @param string $name Attribute name (i.e. "class", "for", etc.)
|
||||
* @param string $value Value to set for the attribute
|
||||
* @param string $markup Markup where the attribute or placeholder exists
|
||||
* @param bool $removeEmpty Remove attribute if it resolves to empty value?
|
||||
* @return string Updated markup
|
||||
* @since 3.0.242
|
||||
*
|
||||
*/
|
||||
protected function setAttributeInMarkup($name, $value, $markup, $removeEmpty = false) {
|
||||
|
||||
$placeholder = '{' . $name . '}';
|
||||
$hasPlaceholder = strpos($markup, $placeholder) !== false;
|
||||
|
||||
if(strlen("$value")) {
|
||||
if($hasPlaceholder) {
|
||||
// replace existing class="… with class="… value
|
||||
$replacement = $name === 'class' ? " $value" : $value;
|
||||
$markup = str_replace($placeholder, $replacement, $markup);
|
||||
|
||||
} else if(strpos($markup, " $name=") !== false) {
|
||||
// update existing attribute value without a {class} being present
|
||||
// for class attribute it appends existing, for others it replaces
|
||||
$replacement = $name === 'class' ? "$1 $value" : $value;
|
||||
$markup = preg_replace('/(\s' . $name . '=[\'"][^\'"]*)/', $replacement, $markup, 1);
|
||||
|
||||
} else {
|
||||
// insert attribute where it doesn't currently exist
|
||||
$markup = preg_replace('!(<[a-z0-9]+)(\s*)!i', "$1 $name='$value'$2", $markup, 1);
|
||||
}
|
||||
|
||||
// remove unnecessary whitespace in class attribute values
|
||||
if($name === 'class') {
|
||||
foreach(array(" $name=' ", " $name=\" ") as $find) {
|
||||
while(strpos($markup, $find)) {
|
||||
$markup = str_replace($find, rtrim($find), $markup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if($hasPlaceholder) {
|
||||
if($removeEmpty) {
|
||||
// remove name="{name}"
|
||||
$markup = str_replace(array(" $name='{" . $name . "}'", " $name=\"{" . $name . "}\""), '', $markup);
|
||||
} else {
|
||||
// replace {name} with blank string
|
||||
$markup = str_replace($placeholder, '', $markup);
|
||||
}
|
||||
|
||||
} else {
|
||||
// $value is empty and there is no placeholder, leave $markup as-is
|
||||
}
|
||||
|
||||
return $markup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove named attribute from given markup
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $markup
|
||||
* @return string
|
||||
* @since 3.0.250
|
||||
*
|
||||
*/
|
||||
protected function removeAttributeFromMarkup($name, $markup) {
|
||||
if(stripos($markup, " $name=") === false) return $markup;
|
||||
return preg_replace('!\s' . $name . '=["\'][^"\']*["\']!i', '', $markup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Inputfield header actions
|
||||
*
|
||||
* @param Inputfield $inputfield
|
||||
* @param array $actions
|
||||
* @return string
|
||||
* @since 3.0.240
|
||||
*
|
||||
*/
|
||||
protected function renderHeaderActions(Inputfield $inputfield, array $actions) {
|
||||
$sanitizer = $this->wire()->sanitizer;
|
||||
$out = '';
|
||||
$modal = false;
|
||||
foreach($actions as $a) {
|
||||
$icon = '';
|
||||
$type = '';
|
||||
if(isset($a['icon'])) {
|
||||
$icon = $a['icon'];
|
||||
if(isset($a['href'])) {
|
||||
$type = 'link';
|
||||
if(!empty($a['modal'])) $modal = true;
|
||||
} else {
|
||||
$type = 'click';
|
||||
}
|
||||
} else if(isset($a['offIcon'])) {
|
||||
$type = 'toggle';
|
||||
if(!isset($a['onIcon'])) $a['onIcon'] = $a['offIcon'];
|
||||
} else if(isset($a['onIcon'])) {
|
||||
$type = 'toggle';
|
||||
$a['offIcon'] = $a['onIcon'];
|
||||
}
|
||||
if($type === 'toggle') $icon = !empty($a['on']) ? $a['onIcon'] : $a['offIcon'];
|
||||
if(empty($icon) || empty($type)) continue;
|
||||
$a['type'] = $type;
|
||||
if(strpos($icon, 'fa-') !== 0) $icon = "fa-$icon";
|
||||
$data = $sanitizer->entities(json_encode($a));
|
||||
$out .= "<i class='_InputfieldHeaderAction fa fa-fw $icon' data-action='$data' hidden></i>";
|
||||
}
|
||||
if($modal) {
|
||||
/** @var JqueryUI $jQueryUI */
|
||||
$jQueryUI = $this->wire()->modules->get('JqueryUI');
|
||||
$jQueryUI->use('modal');
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the output of this Inputfield and its children, showing values only (no inputs)
|
||||
*
|
||||
@@ -1132,7 +1278,22 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
|
||||
$url .= "renderInputfieldAjax=$inputfieldID";
|
||||
$url = $sanitizer->entities($url);
|
||||
|
||||
$out = "<div class='renderInputfieldAjax'><input type='hidden' value='$url' /></div>";
|
||||
$valueInput = '';
|
||||
$val = $inputfield->val();
|
||||
if(!is_array($val) && !is_object($val)) {
|
||||
$val = (string) $val;
|
||||
if(strlen("$val") <= 1024) {
|
||||
// keep value in hidden input so dependences can refer to it
|
||||
$val = $sanitizer->entities("$val");
|
||||
$valueInput = "<input type='hidden' id='$inputfieldID' value='$val' />";
|
||||
}
|
||||
}
|
||||
|
||||
$out =
|
||||
"<div class='renderInputfieldAjax'>" .
|
||||
"<input type='hidden' value='$url' />" .
|
||||
$valueInput .
|
||||
"</div>";
|
||||
|
||||
if($inputfield instanceof InputfieldWrapper) {
|
||||
// load assets they will need
|
||||
@@ -1566,6 +1727,64 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
|
||||
return $inputfield;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Inputfield by Field (hasField)
|
||||
*
|
||||
* This is useful in cases where the input name may differ from the Field name
|
||||
* that it represents, and you only know the field name. Applies only to
|
||||
* Inputfields connected with a Page and Field (i.e. used for page editing).
|
||||
*
|
||||
* #pw-group-retrieval-and-traversal
|
||||
*
|
||||
* @param Field|string|int $field
|
||||
* @return Inputfield|InputfieldWrapper|null
|
||||
* @since 3.0.239
|
||||
*
|
||||
*/
|
||||
public function getByField($field) {
|
||||
if(!$field instanceof Field) $field = $this->wire()->fields->get($field);
|
||||
return $this->getByProperty('hasField', $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Inputfield by some other non-attribute property or setting
|
||||
*
|
||||
* #pw-group-retrieval-and-traversal
|
||||
*
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
* @param bool $getAll Get array of all matching Inputfields rather than just first? (default=false)
|
||||
* @return Inputfield|InputfieldWrapper|null|array
|
||||
* @since 3.0.239
|
||||
*
|
||||
*/
|
||||
public function getByProperty($property, $value, $getAll = false) {
|
||||
$inputfield = null;
|
||||
$value = (string) $value;
|
||||
$a = array();
|
||||
|
||||
foreach($this->children() as $child) {
|
||||
/** @var Inputfield $child */
|
||||
if((string) $child->getSetting($property) === $value) {
|
||||
$inputfield = $child;
|
||||
} else if($child instanceof InputfieldWrapper) {
|
||||
if($getAll) {
|
||||
$a = array_merge($a, $child->getByProperty($property, $value, true));
|
||||
} else {
|
||||
$inputfield = $child->getByProperty($property, $value);
|
||||
}
|
||||
}
|
||||
if($inputfield) {
|
||||
if($getAll) {
|
||||
$a[] = $inputfield;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $getAll ? $a : $inputfield;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value of Inputfield by name
|
||||
*
|
||||
@@ -1684,12 +1903,17 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
|
||||
/** @var InputfieldSelect $f */
|
||||
$f = $inputfields->getChildByName('collapsed');
|
||||
if($f) {
|
||||
// remove all options for 'collapsed' except for a few
|
||||
// whitelist of collapsed options allowed for fieldsets/wrappers
|
||||
$allow = array(
|
||||
Inputfield::collapsedNo,
|
||||
Inputfield::collapsedYes,
|
||||
Inputfield::collapsedYesAjax,
|
||||
Inputfield::collapsedNever,
|
||||
Inputfield::collapsedHidden,
|
||||
Inputfield::collapsedBlank,
|
||||
Inputfield::collapsedPopulated,
|
||||
Inputfield::collapsedBlankAjax,
|
||||
Inputfield::collapsedBlankLocked,
|
||||
);
|
||||
foreach($f->getOptions() as $value => $label) {
|
||||
if(!in_array($value, $allow)) $f->removeOption($value);
|
||||
@@ -1797,11 +2021,11 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
|
||||
* #pw-group-manipulation
|
||||
*
|
||||
* @param array $a Array of Inputfield definitions
|
||||
* @param InputfieldWrapper $inputfields Specify the wrapper you want them added to, or omit to use current.
|
||||
* @param InputfieldWrapper|null $inputfields Specify the wrapper you want them added to, or omit to use current.
|
||||
* @return $this
|
||||
*
|
||||
*/
|
||||
public function importArray(array $a, InputfieldWrapper $inputfields = null) {
|
||||
public function importArray(array $a, ?InputfieldWrapper $inputfields = null) {
|
||||
|
||||
$modules = $this->wire()->modules;
|
||||
|
||||
|
@@ -5,12 +5,18 @@
|
||||
*
|
||||
* The default numeric indexing of a WireArray is not overridden.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*/
|
||||
|
||||
class InputfieldsArray extends WireArray {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->usesNumericKeys = true;
|
||||
$this->indexedByName = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per WireArray interface, only Inputfield instances are accepted.
|
||||
@@ -31,6 +37,7 @@ class InputfieldsArray extends WireArray {
|
||||
*
|
||||
*/
|
||||
public function find($selector) {
|
||||
/** @var WireArray|InputfieldsArray $a */
|
||||
$a = parent::find($selector);
|
||||
foreach($this as $item) {
|
||||
if(!$item instanceof InputfieldWrapper) continue;
|
||||
@@ -44,8 +51,4 @@ class InputfieldsArray extends WireArray {
|
||||
return null; // Inputfield is abstract, so there is nothing to return here
|
||||
}
|
||||
|
||||
public function usesNumericKeys() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -236,6 +236,58 @@ interface FieldtypeHasPageimages {
|
||||
public function getPageimages(Page $page, Field $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates Fieldtype has version support and manages its own versions
|
||||
*
|
||||
*/
|
||||
interface FieldtypeDoesVersions {
|
||||
|
||||
/**
|
||||
* Get the value for given page, field and version
|
||||
*
|
||||
* @param Page $page
|
||||
* @param Field $field
|
||||
* @param int $version
|
||||
* @return mixed
|
||||
*
|
||||
*/
|
||||
public function getPageFieldVersion(Page $page, Field $field, $version);
|
||||
|
||||
/**
|
||||
* Save version of given page field
|
||||
*
|
||||
* @param Page $page
|
||||
* @param Field $field
|
||||
* @param int $version
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function savePageFieldVersion(Page $page, Field $field, $version);
|
||||
|
||||
/**
|
||||
* Restore version of given page field to live page
|
||||
*
|
||||
* @param Page $page
|
||||
* @param Field $field
|
||||
* @param int $version
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function restorePageFieldVersion(Page $page, Field $field, $version);
|
||||
|
||||
/**
|
||||
* Delete version
|
||||
*
|
||||
* @param Page $page
|
||||
* @param Field $field
|
||||
* @param int $version
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function deletePageFieldVersion(Page $page, Field $field, $version);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Indicates that an Inputfield provides tree selection capabilities
|
||||
*
|
||||
@@ -706,7 +758,7 @@ interface InputfieldHasSelectableOptions {
|
||||
* @return self|$this
|
||||
*
|
||||
*/
|
||||
public function addOption($value, $label = null, array $attributes = null);
|
||||
public function addOption($value, $label = null, ?array $attributes = null);
|
||||
|
||||
/**
|
||||
* Add selectable option with label, optionally for specific language
|
||||
|
@@ -74,12 +74,12 @@ class MarkupFieldtype extends WireData implements Module {
|
||||
* If you construct without providing page and field, please populate them
|
||||
* separately with the setPage and setField methods before calling render().
|
||||
*
|
||||
* @param Page $page
|
||||
* @param Field $field
|
||||
* @param Page|null $page
|
||||
* @param Field|null $field
|
||||
* @param mixed $value
|
||||
*
|
||||
*/
|
||||
public function __construct(Page $page = null, Field $field = null, $value = null) {
|
||||
public function __construct(?Page $page = null, ?Field $field = null, $value = null) {
|
||||
parent::__construct();
|
||||
if($page) $this->setPage($page);
|
||||
if($field) $this->setField($field);
|
||||
@@ -266,11 +266,14 @@ class MarkupFieldtype extends WireData implements Module {
|
||||
*
|
||||
*/
|
||||
protected function valueToString($value, $encode = true) {
|
||||
if($value instanceof Pagefiles || $value instanceof Pagefile) {
|
||||
$isObject = is_object($value);
|
||||
if($isObject && ($value instanceof Pagefiles || $value instanceof Pagefile)) {
|
||||
return $this->objectToString($value);
|
||||
} else if($isObject && wireInstanceOf($value, 'RepeaterPageArray')) {
|
||||
return $this->objectToString($value);
|
||||
} else if(WireArray::iterable($value)) {
|
||||
return $this->arrayToString($value);
|
||||
} else if(is_object($value)) {
|
||||
} else if($isObject) {
|
||||
return $this->objectToString($value);
|
||||
} else {
|
||||
return $encode ? $this->wire()->sanitizer->entities1($value) : $value;
|
||||
@@ -303,11 +306,16 @@ class MarkupFieldtype extends WireData implements Module {
|
||||
*
|
||||
*/
|
||||
protected function objectToString($value) {
|
||||
if($value instanceof WireArray) {
|
||||
if($value instanceof WireArray) {
|
||||
if(!$value->count()) return '';
|
||||
if(wireInstanceOf($value, 'RepeaterPageArray')) {
|
||||
return $this->renderInputfieldValue($value);
|
||||
}
|
||||
}
|
||||
if($value instanceof Page) {
|
||||
if($value->viewable()) {
|
||||
if(wireInstanceOf($value, 'FieldsetPage')) {
|
||||
return $this->renderInputfieldValue($value);
|
||||
} else if($value->viewable()) {
|
||||
return "<a href='$value->url'>" . $value->get('title|name') . "</a>";
|
||||
} else {
|
||||
return $value->get('title|name');
|
||||
@@ -385,7 +393,7 @@ class MarkupFieldtype extends WireData implements Module {
|
||||
*
|
||||
*/
|
||||
public function __toString() {
|
||||
return $this->render();
|
||||
return (string) $this->render();
|
||||
}
|
||||
|
||||
public function setPage(Page $page) { $this->_page = $page; }
|
||||
|
@@ -67,11 +67,11 @@ class MarkupQA extends Wire {
|
||||
/**
|
||||
* Construct
|
||||
*
|
||||
* @param Page $page
|
||||
* @param Field $field
|
||||
* @param Page|null $page
|
||||
* @param Field|null $field
|
||||
*
|
||||
*/
|
||||
public function __construct(Page $page = null, Field $field = null) {
|
||||
public function __construct(?Page $page = null, ?Field $field = null) {
|
||||
parent::__construct();
|
||||
if($page) {
|
||||
$this->setPage($page);
|
||||
@@ -509,7 +509,6 @@ class MarkupQA extends Wire {
|
||||
$replacements = array();
|
||||
$languages = $this->wire()->languages;
|
||||
$config = $this->wire()->config;
|
||||
$pages = $this->wire()->pages;
|
||||
$rootURL = $config->urls->root;
|
||||
$adminURL = $config->urls->admin;
|
||||
$adminPath = $rootURL === '/' ? $adminURL : str_replace($rootURL, '/', $adminURL);
|
||||
@@ -542,10 +541,8 @@ class MarkupQA extends Wire {
|
||||
} else {
|
||||
$language = null;
|
||||
}
|
||||
|
||||
$livePath = $pages->getPath($pageID, array(
|
||||
'language' => $language
|
||||
));
|
||||
|
||||
$livePath = $this->getPagePathFromId($pageID, $language);
|
||||
|
||||
if($urlSegmentStr) {
|
||||
$livePath = rtrim($livePath, '/') . "/$urlSegmentStr";
|
||||
@@ -609,7 +606,7 @@ class MarkupQA extends Wire {
|
||||
/**
|
||||
* Find pages linking to another
|
||||
*
|
||||
* @param Page $page Page to find links to, or omit to use page specified in constructor
|
||||
* @param Page|null $page Page to find links to, or omit to use page specified in constructor
|
||||
* @param array $fieldNames Field names to look in or omit to use field specified in constructor
|
||||
* @param string $selector Optional selector to use as a filter
|
||||
* @param array $options Additional options
|
||||
@@ -620,7 +617,7 @@ class MarkupQA extends Wire {
|
||||
* @return PageArray|array|int
|
||||
*
|
||||
*/
|
||||
public function findLinks(Page $page = null, $fieldNames = array(), $selector = '', array $options = array()) {
|
||||
public function findLinks(?Page $page = null, $fieldNames = array(), $selector = '', array $options = array()) {
|
||||
|
||||
$pages = $this->wire()->pages;
|
||||
$fields = $this->wire()->fields;
|
||||
@@ -1026,4 +1023,67 @@ class MarkupQA extends Wire {
|
||||
$this->settings['verbose'] = $verbose ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given page ID return the path to it
|
||||
*
|
||||
* @param int $pageID
|
||||
* @param Language|null $language
|
||||
* @return string
|
||||
* @since 3.0.231
|
||||
*
|
||||
*/
|
||||
protected function getPagePathFromId($pageID, $language = null) {
|
||||
|
||||
$pages = $this->wire()->pages;
|
||||
$path = null;
|
||||
|
||||
if($this->isPagePathHooked()) {
|
||||
$page = $pages->get($pageID);
|
||||
if($page->id) {
|
||||
if($language && $language->id) {
|
||||
$languages = $this->wire()->languages;
|
||||
$languages->setLanguage($language);
|
||||
$path = $page->path();
|
||||
$languages->unsetLanguage();
|
||||
} else {
|
||||
$path = $page->path();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($path === null) {
|
||||
$path = $pages->getPath($pageID, array(
|
||||
'language' => $language
|
||||
));
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the Page::path method hooked in a manner that might affect MarkupQA?
|
||||
*
|
||||
* @return bool
|
||||
* @since 3.0.231
|
||||
*
|
||||
*/
|
||||
protected function isPagePathHooked() {
|
||||
$config = $this->wire()->config;
|
||||
$property = '_MarkupQA_pagePathHooked';
|
||||
$hooked = $config->get($property);
|
||||
if($hooked !== null) return $hooked;
|
||||
$hooks = $this->wire()->hooks;
|
||||
$hooked = $hooks->isHooked('Page::path()');
|
||||
if($hooked) {
|
||||
// only consider Page::path hooked if something other than LanguageSupportPageNames hooks it
|
||||
$hookItems = $hooks->getHooks($this->page, 'path', WireHooks::getHooksStatic);
|
||||
foreach($hookItems as $key => $hook) {
|
||||
if(((string) $hook['toObject']) === 'LanguageSupportPageNames') unset($hookItems[$key]);
|
||||
}
|
||||
$hooked = count($hookItems) > 0;
|
||||
}
|
||||
$config->setQuietly($property, $hooked);
|
||||
return $hooked;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -270,7 +270,7 @@
|
||||
* - Singular modules will have their instance active for the entire request after instantiated.
|
||||
* - Non-singular modules return a new instance on every `$modules->get("YourModule")` call.
|
||||
* - Modules that attach hooks are usually singular.
|
||||
* - Modules that may have multiple instances (like `Inputfield` modules) should _not_be singular.
|
||||
* - Modules that may have multiple instances (like `Inputfield` modules) should _not_ be singular.
|
||||
*
|
||||
* If you are having trouble deciding whether to make your module singular or not, be sure to read
|
||||
* the documentation below for the `isAutoload()` method, because if your module is 'autoload' then
|
||||
@@ -588,4 +588,3 @@ interface SearchableModule {
|
||||
*/
|
||||
public function search($text, array $options = array());
|
||||
}
|
||||
|
||||
|
@@ -236,6 +236,9 @@ class Modules extends WireArray {
|
||||
*/
|
||||
public function __construct($path) {
|
||||
parent::__construct();
|
||||
$this->nameProperty = 'className';
|
||||
$this->usesNumericKeys = false;
|
||||
$this->indexedByName = true;
|
||||
$this->addPath($path); // paths[0] is always core modules path
|
||||
}
|
||||
|
||||
@@ -733,8 +736,8 @@ class Modules extends WireArray {
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string|object $moduleName Module instance or module name
|
||||
* @param User $user Optionally specify different user to consider than current.
|
||||
* @param Page $page Optionally specify different page to consider than current.
|
||||
* @param User|null $user Optionally specify different user to consider than current.
|
||||
* @param Page|null $page Optionally specify different page to consider than current.
|
||||
* @param bool $strict If module specifies no permission settings, assume no permission.
|
||||
* - Default (false) is to assume permission when module doesn't say anything about it.
|
||||
* - Process modules (for instance) generally assume no permission when it isn't specifically defined
|
||||
@@ -743,7 +746,7 @@ class Modules extends WireArray {
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function hasPermission($moduleName, User $user = null, Page $page = null, $strict = false) {
|
||||
public function hasPermission($moduleName, ?User $user = null, ?Page $page = null, $strict = false) {
|
||||
return $this->loader->hasPermission($moduleName, $user, $page, $strict);
|
||||
}
|
||||
|
||||
@@ -978,7 +981,7 @@ class Modules extends WireArray {
|
||||
$currentVersion = $class === 'PHP' ? PHP_VERSION : $this->wire()->config->version;
|
||||
}
|
||||
} else {
|
||||
$installed = parent::get($class) !== null;
|
||||
$installed = isset($this->data[$class]);
|
||||
if($installed && $requiredVersion !== null) {
|
||||
$info = $this->info->getModuleInfo($class);
|
||||
$currentVersion = $info['version'];
|
||||
@@ -1233,12 +1236,14 @@ class Modules extends WireArray {
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string|int|Module $name
|
||||
* @param string|Module $name
|
||||
* @param int|null|false $setID Optionally set module ID or false to unset
|
||||
* @return int
|
||||
*
|
||||
*/
|
||||
public function moduleID($name, $setID = null) {
|
||||
if($name instanceof Module) $name = $name->className();
|
||||
if(strpos("$name", '\\') !== false) $name = wireClassName($name, false);
|
||||
if($setID !== null) {
|
||||
if($setID === false) {
|
||||
unset($this->moduleIDs[$name]);
|
||||
@@ -1269,7 +1274,8 @@ class Modules extends WireArray {
|
||||
if($setName === null) return $name;
|
||||
$id = $this->getModuleID($name);
|
||||
} else if(!ctype_digit("$id")) {
|
||||
if(is_string($id)) return $id;
|
||||
if(strpos("$id", '\\') !== false) $id = wireClassName($id, false);
|
||||
if($setName === null && is_string($id)) return $id;
|
||||
$id = $this->getModuleID($id);
|
||||
}
|
||||
$id = (int) $id;
|
||||
@@ -1291,23 +1297,17 @@ class Modules extends WireArray {
|
||||
|
||||
$id = 0;
|
||||
|
||||
if(ctype_digit("$class")) {
|
||||
return (int) $class;
|
||||
} else if(isset($this->moduleIDs["$class"])) {
|
||||
return (int) $this->moduleIDs["$class"];
|
||||
}
|
||||
|
||||
if(ctype_digit("$class")) return (int) $class;
|
||||
if(isset($this->moduleIDs["$class"])) return (int) $this->moduleIDs["$class"];
|
||||
|
||||
if(is_object($class)) {
|
||||
if($class instanceof Module) {
|
||||
$class = $this->getModuleClass($class);
|
||||
if(isset($this->moduleIDs[$class])) {
|
||||
return (int) $this->moduleIDs[$class];
|
||||
}
|
||||
} else {
|
||||
// Class is not a module
|
||||
return $id;
|
||||
}
|
||||
if(!$class instanceof Module) return 0; // class is not a module
|
||||
$class = $this->getModuleClass($class);
|
||||
} else if(strpos("$class", '\\') !== false) {
|
||||
$class = wireClassName($class, false);
|
||||
}
|
||||
|
||||
if(isset($this->moduleIDs["$class"])) return (int) $this->moduleIDs["$class"];
|
||||
|
||||
foreach($this->info->moduleInfoCache as $key => $info) {
|
||||
if(is_string($info)) {
|
||||
@@ -1369,26 +1369,26 @@ class Modules extends WireArray {
|
||||
if(strpos($module, '.') !== false) {
|
||||
$module = basename(basename($module, '.php'), '.module');
|
||||
}
|
||||
|
||||
if(array_key_exists($module, $this->moduleIDs)) {
|
||||
|
||||
if(isset($this->data[$module])) {
|
||||
$className = $module;
|
||||
} else if(array_key_exists($module, $this->moduleIDs)) {
|
||||
$className = $module;
|
||||
} else if(array_key_exists($module, $this->installableFiles)) {
|
||||
$className = $module;
|
||||
}
|
||||
}
|
||||
|
||||
if($className) {
|
||||
if($withNamespace) {
|
||||
if($namespace) {
|
||||
$className = "$namespace\\$className";
|
||||
} else {
|
||||
$className = $this->info->getModuleNamespace($className) . $className;
|
||||
}
|
||||
if(!$className) return false;
|
||||
|
||||
if($withNamespace) {
|
||||
if($namespace) {
|
||||
$className = "$namespace\\$className";
|
||||
} else {
|
||||
$className = $this->info->getModuleNamespace($className) . $className;
|
||||
}
|
||||
return $className;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return $className;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1740,7 +1740,7 @@ class Modules extends WireArray {
|
||||
* @return InputfieldWrapper|null
|
||||
*
|
||||
*/
|
||||
public function ___getModuleConfigInputfields($moduleName, InputfieldWrapper $form = null) {
|
||||
public function ___getModuleConfigInputfields($moduleName, ?InputfieldWrapper $form = null) {
|
||||
return $this->configs->getModuleConfigInputfields($moduleName, $form);
|
||||
}
|
||||
|
||||
|
@@ -558,7 +558,7 @@ class ModulesConfigs extends ModulesClass {
|
||||
* @return InputfieldWrapper|null
|
||||
*
|
||||
*/
|
||||
public function getModuleConfigInputfields($moduleName, InputfieldWrapper $form = null) {
|
||||
public function getModuleConfigInputfields($moduleName, ?InputfieldWrapper $form = null) {
|
||||
|
||||
$moduleName = $this->modules->getModuleClass($moduleName);
|
||||
$configurable = $this->isConfigurable($moduleName);
|
||||
|
@@ -309,6 +309,10 @@ class ModulesFiles extends ModulesClass {
|
||||
if(class_exists(__NAMESPACE__ . "\\$moduleName", false)) return true;
|
||||
// next do a slower check, figuring out namespace
|
||||
$ns = $this->modules->info->getModuleNamespace($moduleName, array('file' => $file));
|
||||
if($ns === null) {
|
||||
// unable to determine module namespace, likely file does not exist
|
||||
$ns = (string) $ns;
|
||||
}
|
||||
$className = trim($ns, "\\") . "\\$moduleName";
|
||||
if(class_exists($className, false)) return true;
|
||||
// if this point is reached, module is not yet in memory in either instance
|
||||
@@ -430,6 +434,7 @@ class ModulesFiles extends ModulesClass {
|
||||
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
|
||||
|
||||
$class = $row['class'];
|
||||
if(strpos($class, '.') === 0) continue;
|
||||
|
||||
$file = $this->getModuleFile($class, array('fast' => true));
|
||||
|
||||
|
@@ -1364,7 +1364,7 @@ class ModulesInfo extends ModulesClass {
|
||||
*
|
||||
*/
|
||||
public function getNamespacePath($namespace) {
|
||||
if($namespace === 'ProcessWire') return "ProcessWire\\";
|
||||
if($namespace === 'ProcessWire') return false; // not unique module namespace
|
||||
if(is_null($this->moduleNamespaceCache)) $this->getNamespaces();
|
||||
$namespace = "\\" . trim($namespace, "\\") . "\\";
|
||||
return isset($this->moduleNamespaceCache[$namespace]) ? $this->moduleNamespaceCache[$namespace] : false;
|
||||
|
@@ -203,9 +203,11 @@ class ModulesInstaller extends ModulesClass {
|
||||
$permissions->save($permission);
|
||||
if($languages) $languages->unsetDefault();
|
||||
$this->message(sprintf($this->_('Added Permission: %s'), $permission->name));
|
||||
if($languages) $languages->setDefault();
|
||||
} catch(\Exception $e) {
|
||||
if($languages) $languages->unsetDefault();
|
||||
$error = sprintf($this->_('Error adding permission: %s'), $name);
|
||||
if($languages) $languages->setDefault();
|
||||
$this->trackException($e, false, $error);
|
||||
}
|
||||
}
|
||||
|
@@ -412,7 +412,7 @@ class ModulesLoader extends ModulesClass {
|
||||
$requires = array();
|
||||
$name = $moduleName;
|
||||
$moduleName = $this->loadModule($path, $pathname, $requires, $installed);
|
||||
if(!$config->paths->get($name)) $modulesFiles->setConfigPaths($name, dirname($basePath . $pathname));
|
||||
if(!$config->paths->__isset($name)) $modulesFiles->setConfigPaths($name, dirname($basePath . $pathname));
|
||||
if(!$moduleName) continue;
|
||||
|
||||
if(count($requires)) {
|
||||
@@ -737,8 +737,8 @@ class ModulesLoader extends ModulesClass {
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string|object $moduleName Module instance or module name
|
||||
* @param User $user Optionally specify different user to consider than current.
|
||||
* @param Page $page Optionally specify different page to consider than current.
|
||||
* @param User|null $user Optionally specify different user to consider than current.
|
||||
* @param Page|null $page Optionally specify different page to consider than current.
|
||||
* @param bool $strict If module specifies no permission settings, assume no permission.
|
||||
* - Default (false) is to assume permission when module doesn't say anything about it.
|
||||
* - Process modules (for instance) generally assume no permission when it isn't specifically defined
|
||||
@@ -747,7 +747,7 @@ class ModulesLoader extends ModulesClass {
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function hasPermission($moduleName, User $user = null, Page $page = null, $strict = false) {
|
||||
public function hasPermission($moduleName, ?User $user = null, ?Page $page = null, $strict = false) {
|
||||
|
||||
if(is_object($moduleName)) {
|
||||
$module = $moduleName;
|
||||
|
@@ -507,6 +507,12 @@ class NoticeWarning extends Notice {
|
||||
class Notices extends WireArray {
|
||||
|
||||
const logAllNotices = false; // for debugging/dev purposes
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->usesNumericKeys = true;
|
||||
$this->indexedByName = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Notices API var
|
||||
@@ -548,8 +554,9 @@ class Notices extends WireArray {
|
||||
*
|
||||
*/
|
||||
protected function allowNotice(Notice $item) {
|
||||
|
||||
$user = $this->wire()->user;
|
||||
|
||||
// intentionally not using $this->wire()->user; in case this gets called early in boot
|
||||
$user = $this->wire('user');
|
||||
|
||||
if($item->flags & Notice::debug) {
|
||||
if(!$this->wire()->config->debug) return false;
|
||||
|
@@ -65,6 +65,7 @@ class NullPage extends Page implements WireNull {
|
||||
*
|
||||
* @param string $selector
|
||||
* @return null
|
||||
* @todo can this return NullPage instead?
|
||||
*
|
||||
*/
|
||||
public function parent($selector = '') { return null; }
|
||||
@@ -97,6 +98,14 @@ class NullPage extends Page implements WireNull {
|
||||
*/
|
||||
public function isHidden() { return true; }
|
||||
|
||||
/**
|
||||
* #pw-internal
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function isNew() { return false; }
|
||||
|
||||
/**
|
||||
* #pw-internal
|
||||
*
|
||||
|
@@ -8,7 +8,7 @@
|
||||
* 1. Providing get/set access to the Page's properties
|
||||
* 2. Accessing the related hierarchy of pages (i.e. parents, children, sibling pages)
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* #pw-summary Class used by all Page objects in ProcessWire.
|
||||
@@ -68,8 +68,9 @@
|
||||
* @property string $filesPath Get the disk path to store files for this page, creating it if it does not exist. #pw-group-files
|
||||
* @property string $filesUrl Get the URL to store files for this page, creating it if it does not exist. #pw-group-files
|
||||
* @property bool $hasFiles Does this page have one or more files in its files path? #pw-group-files
|
||||
* @property bool $outputFormatting Whether output formatting is enabled or not. #pw-advanced
|
||||
* @property bool $outputFormatting Whether output formatting is enabled or not. Same as calling $page->of() with no arguments. #pw-advanced
|
||||
* @property int $sort Sort order of this page relative to siblings (applicable when manual sorting is used). #pw-group-system
|
||||
* @property int|null $sortPrevious Previous sort order, if changed (3.0.235+) #pw-group-system
|
||||
* @property int $index Index of this page relative to its siblings, regardless of sort (starting from 0). #pw-group-traversal
|
||||
* @property string $sortfield Field that a page is sorted by relative to its siblings (default="sort", which means drag/drop manual) #pw-group-system
|
||||
* @property null|array _statusCorruptedFields Field names that caused the page to have Page::statusCorrupted status. #pw-internal
|
||||
@@ -104,7 +105,7 @@
|
||||
*
|
||||
* Methods added by PageRender.module:
|
||||
* -----------------------------------
|
||||
* @method string|mixed render($fieldName = '') Returns rendered page markup. If given a $fieldName argument, it behaves same as the renderField() method. #pw-group-output-rendering
|
||||
* @method string|mixed render($arg1 = null, $arg2 = null) Returns rendered page markup. Please see the `PageRender::renderPage()` method for arguments and usage details. #pw-group-output-rendering
|
||||
*
|
||||
* Methods added by PagePermissions.module:
|
||||
* ----------------------------------------
|
||||
@@ -119,6 +120,7 @@
|
||||
* @method bool addable($pageToAdd = null) Returns true if the current user can add children to the page, false if not. Optionally specify the page to be added for additional access checking. #pw-group-access
|
||||
* @method bool moveable($newParent = null) Returns true if the current user can move this page. Optionally specify the new parent to check if the page is moveable to that parent. #pw-group-access
|
||||
* @method bool sortable() Returns true if the current user can change the sort order of the current page (within the same parent). #pw-group-access
|
||||
* @method bool cloneable($recursive = null) Can current user clone this page? Specify false for $recursive argument to ignore whether children are cloneable. @since 3.0.239 #pw-group-access
|
||||
* @property bool $viewable #pw-group-access
|
||||
* @property bool $editable #pw-group-access
|
||||
* @property bool $publishable #pw-group-access
|
||||
@@ -129,6 +131,7 @@
|
||||
* @property bool $moveable #pw-group-access
|
||||
* @property bool $sortable #pw-group-access
|
||||
* @property bool $listable #pw-group-access
|
||||
* @property bool $cloneable @since 3.0.239 #pw-group-access
|
||||
*
|
||||
* Methods added by PagePathHistory.module (installed by default)
|
||||
* --------------------------------------------------------------
|
||||
@@ -139,7 +142,9 @@
|
||||
* Methods added by LanguageSupport.module (not installed by default)
|
||||
* -----------------------------------------------------------------
|
||||
* @method Page setLanguageValue($language, $field, $value) Set value for field in language (requires LanguageSupport module). $language may be ID, language name or Language object. Field should be field name (string). #pw-group-languages
|
||||
* @method Page getLanguageValue($language, $field) Get value for field in language (requires LanguageSupport module). $language may be ID, language name or Language object. Field should be field name (string). #pw-group-languages
|
||||
* @method Page setLanguageValues($field, array $values) Set value for field in one or more languages (requires LanguageSupport module). $field should be field/property name (string), $values should be array of values indexed by language name. @since 3.0.236 #pw-group-languages
|
||||
* @method mixed getLanguageValue($language, $field) Get value for field in language (requires LanguageSupport module). $language may be ID, language name or Language object. Field should be field name (string). #pw-group-languages
|
||||
* @method array getLanguageValues($field, array $langs = []) Get values for field or one or more languages (requires LanguageSupport module). $field should be field/property name (string), $langs should be array of language names, or omit for all languages. Returns array of values indexed by language name. @since 3.0.236 #pw-group-languages
|
||||
*
|
||||
* Methods added by LanguageSupportPageNames.module (not installed by default)
|
||||
* ---------------------------------------------------------------------------
|
||||
@@ -147,6 +152,10 @@
|
||||
* @method string localPath($language = null) Return the page path in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages #pw-group-urls
|
||||
* @method string localUrl($language = null) Return the page URL in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages #pw-group-urls
|
||||
* @method string localHttpUrl($language = null) Return the page URL (including scheme and hostname) in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages #pw-group-urls
|
||||
* @method Page setLanguageStatus($language, $status = null) Set active status for language(s), can be called as `$page->setLanguageStatus('es', true);` or `$page->setLanguageStatus([ 'es' => true, 'br' => false ]);` to set multiple. @since 3.0.236 #pw-group-languages
|
||||
* @method array|bool getLanguageStatus($language = []) Get active status for language(s). If given a $language (Language or name of language) it returns a boolean. If given multiple language names (array), or argument omitted, it returns array like `[ 'default' => true, 'fr' => false ];`. @since 3.0.236 #pw-group-languages
|
||||
* @method Page setLanguageName($language, $name = null) Set page name for language with `$page->setLanguageName('es', 'hola');` or set multiple with `$page->setLanguageName([ 'default' => 'hello', 'es' => 'hola' ]);` @since 3.0.236 #pw-group-languages
|
||||
* @method array|string getLanguageName($language = []) Get page name for language(s). If given a Language object, it returns a string. If given array of language names, or argument omitted, it returns an array like `[ 'default' => 'hello', 'es' => 'hola' ];`. @since 3.0.236 #pw-group-languages
|
||||
*
|
||||
* Methods added by PageFrontEdit.module (not always installed by default)
|
||||
* -----------------------------------------------------------------------
|
||||
@@ -154,7 +163,7 @@
|
||||
*
|
||||
* Methods added by ProDrafts.module (if installed)
|
||||
* ------------------------------------------------
|
||||
* @method ProDraft|\ProDraft|int|string|Page|array draft($key = null, $value = null) Helper method for drafts (added by ProDrafts). #pw-advanced
|
||||
* @method ProDraft|int|string|Page|array draft($key = null, $value = null) Helper method for drafts (added by ProDrafts). #pw-advanced
|
||||
*
|
||||
* Hookable methods
|
||||
* ----------------
|
||||
@@ -377,7 +386,15 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* @var string
|
||||
*
|
||||
*/
|
||||
private $namePrevious;
|
||||
private $namePrevious;
|
||||
|
||||
/**
|
||||
* The previous sort value used by page, if changed during runtime.
|
||||
*
|
||||
* @var int
|
||||
*
|
||||
*/
|
||||
private $sortPrevious;
|
||||
|
||||
/**
|
||||
* The previous status used by this page, if it changed during runtime.
|
||||
@@ -588,10 +605,10 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
/**
|
||||
* Create a new page in memory.
|
||||
*
|
||||
* @param Template $tpl Template object this page should use.
|
||||
* @param Template|null $tpl Template object this page should use.
|
||||
*
|
||||
*/
|
||||
public function __construct(Template $tpl = null) {
|
||||
public function __construct(?Template $tpl = null) {
|
||||
parent::__construct();
|
||||
if($tpl !== null) {
|
||||
$tpl->wire($this);
|
||||
@@ -601,6 +618,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
$this->parentPrevious = null;
|
||||
$this->templatePrevious = null;
|
||||
$this->statusPrevious = null;
|
||||
$this->sortPrevious = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -700,7 +718,8 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
$this->setStatus($value);
|
||||
break;
|
||||
case 'statusPrevious':
|
||||
$this->statusPrevious = is_null($value) ? null : (int) $value;
|
||||
case 'sortPrevious':
|
||||
$this->$key = is_null($value) ? null : (int) $value;
|
||||
break;
|
||||
case 'name':
|
||||
$this->setName($value);
|
||||
@@ -1150,6 +1169,58 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
return $this->values()->getDotValue($this, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload multiple fields together as a group (experimental)
|
||||
*
|
||||
* This is an optimization that enables you to load the values for multiple fields into
|
||||
* a page at once, and often in a single query. For fields where it is supported, and
|
||||
* for cases where you have a lot of fields to load at once, it can be up to 50% faster
|
||||
* than the default of lazy-loading fields.
|
||||
*
|
||||
* To use, call `$page->preload([ 'field1', 'field2', 'etc.' ])` before accessing
|
||||
* `$page->field1`, `$page->field2`, etc.
|
||||
*
|
||||
* The more fields you give this method, the more performance improvement it can offer.
|
||||
* As a result, don't bother if with only a few fields, as it's less likely to make
|
||||
* a difference at small scale. You will also see a more measurable benefit if preloading
|
||||
* fields for lots of pages at once.
|
||||
*
|
||||
* Preload works with some Fieldtypes and not others. For details on what it is doing,
|
||||
* specify `true` for the `debug` option which will make it return array of what it
|
||||
* loaded and what it didn't. Have a look at this array with TracyDebugger or output
|
||||
* a print_r() call on it, and the result is self explanatory.
|
||||
*
|
||||
* NOTE: This function is currently experimental, recommended for testing only.
|
||||
*
|
||||
* ~~~~~
|
||||
* // Example usage
|
||||
* $page->preload([ 'headline', 'body', 'sidebar', 'intro', 'summary' ]);
|
||||
* echo "
|
||||
* <h1 id='headline'>$page->headline</h1>";
|
||||
* <div id='intro'>$page->intro</div>
|
||||
* <div id='body'>$page->body</div>
|
||||
* <aside id='sidebar' pw-append>$page->sidebar</aside>
|
||||
* <meta id='meta-description'>$page->summary</meta>
|
||||
* ";
|
||||
* ~~~~~
|
||||
*
|
||||
* @param array $fieldNames Names of fields to preload or omit (or blank array)
|
||||
* to preload all supported fields.
|
||||
* @param array $options Options to modify default behavior:
|
||||
* - `debug` (bool): Specify true to return additional info in returned array (default=false).
|
||||
* - See the `PagesLoader::preloadFields()` method for additional options.
|
||||
* @return array Array of details
|
||||
* @since 3.0.243
|
||||
*
|
||||
*/
|
||||
public function preload(array $fieldNames = array(), $options = array()) {
|
||||
if(empty($fieldNames)) {
|
||||
return $this->wire()->pages->loader()->preloadAllFields($this, $options);
|
||||
} else {
|
||||
return $this->wire()->pages->loader()->preloadFields($this, $fieldNames, $options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hookable method called when a request to a field was made that didn't match anything
|
||||
*
|
||||
@@ -1819,6 +1890,8 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* - When a string or array, a selector is assumed and quantity will be counted based on selector.
|
||||
* - When boolean true, number includes only visible children (excludes unpublished, hidden, no-access, etc.)
|
||||
* - When boolean false, number includes all children without conditions, including unpublished, hidden, no-access, etc.
|
||||
* - When integer 1 number includes “viewable” children (as opposed to “visible” children, viewable children includes
|
||||
* hidden pages and also includes unpublished pages if user has page-edit permission).
|
||||
* @return int Number of children
|
||||
* @see Page::hasChildren(), Page::children(), Page::child()
|
||||
*
|
||||
@@ -2122,7 +2195,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* @return Page|NullPage Returns the next sibling page, or a NullPage if none found.
|
||||
*
|
||||
*/
|
||||
public function next($selector = '', PageArray $siblings = null) {
|
||||
public function next($selector = '', ?PageArray $siblings = null) {
|
||||
if($selector instanceof PageArray) {
|
||||
$siblings = $selector;
|
||||
$selector = '';
|
||||
@@ -2179,7 +2252,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* @return PageArray
|
||||
*
|
||||
*/
|
||||
public function nextUntil($selector = '', $filter = '', PageArray $siblings = null) {
|
||||
public function nextUntil($selector = '', $filter = '', ?PageArray $siblings = null) {
|
||||
if($siblings === null && $this->traversalPages) $siblings = $this->traversalPages;
|
||||
if($siblings) return $this->traversal()->nextUntilSiblings($this, $selector, $filter, $siblings);
|
||||
return $this->traversal()->nextUntil($this, $selector, $filter);
|
||||
@@ -2203,7 +2276,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* @return Page|NullPage Returns the previous sibling page, or a NullPage if none found.
|
||||
*
|
||||
*/
|
||||
public function prev($selector = '', PageArray $siblings = null) {
|
||||
public function prev($selector = '', ?PageArray $siblings = null) {
|
||||
if($selector instanceof PageArray) {
|
||||
$siblings = $selector;
|
||||
$selector = '';
|
||||
@@ -2242,7 +2315,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* @return PageArray
|
||||
*
|
||||
*/
|
||||
public function prevUntil($selector = '', $filter = '', PageArray $siblings = null) {
|
||||
public function prevUntil($selector = '', $filter = '', ?PageArray $siblings = null) {
|
||||
if($siblings === null && $this->traversalPages) $siblings = $this->traversalPages;
|
||||
if($siblings) return $this->traversal()->prevUntilSiblings($this, $selector, $filter, $siblings);
|
||||
return $this->traversal()->prevUntil($this, $selector, $filter);
|
||||
@@ -2329,7 +2402,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* @param array $options See Pages::save() documentation for options. You may also specify $options as the first argument if no $field is needed.
|
||||
* @return bool Returns true on success false on fail
|
||||
* @throws WireException on database error
|
||||
* @see Pages::save(), Pages::saveField(), Pages::saveReady(), Pages::saveFieldReady(), Pages::saved(), Pages::fieldSaved()
|
||||
* @see Pages::save(), Page::saveFields(), Pages::saveField(), Pages::saveReady(), Pages::saveFieldReady(), Pages::saved(), Pages::fieldSaved()
|
||||
*
|
||||
*/
|
||||
public function save($field = null, array $options = array()) {
|
||||
@@ -2355,6 +2428,21 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
|
||||
return $pages->save($this, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save only the given named fields for this page
|
||||
*
|
||||
* @param array|string $fields Array of field name(s) or string (CSV or space separated)
|
||||
* @param array $options See Pages::save() documentation for options.
|
||||
* @return array Names of fields that were saved
|
||||
* @throws WireException on database error
|
||||
* @see Page::save()
|
||||
* @since 3.0.242
|
||||
*
|
||||
*/
|
||||
public function saveFields($fields, array $options = array()) {
|
||||
return $this->wire()->pages->saveFields($this, $fields, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quickly set field value(s) and save to database
|
||||
@@ -2406,6 +2494,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
if($of) $this->of(false);
|
||||
foreach($values as $k => $v) {
|
||||
$this->set($k, $v);
|
||||
if(!$property) $this->trackChange($k);
|
||||
}
|
||||
if($property) {
|
||||
$result = $this->save($property, $options);
|
||||
@@ -2843,6 +2932,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* - `http` (bool): True to force scheme and hostname in URL (default=auto detect).
|
||||
* - `language` (Language|bool): Optionally specify Language to start editor in, or boolean true to force current user language.
|
||||
* - `find` (string): Name of field to find in the editor (3.0.151+)
|
||||
* - `vars` (array): Additional variables to include in query string (3.0.239+)
|
||||
* @return string URL for editing this page
|
||||
*
|
||||
*/
|
||||
@@ -2929,16 +3019,18 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* - `$page->render->fieldName;`
|
||||
* - `$page->_fieldName_;`
|
||||
*
|
||||
* This method expects that there is a file in `/site/templates/fields/` to render the field with:
|
||||
* This method expects that there is a file in `/site/templates/fields/` to render the field with
|
||||
* one of the following:
|
||||
*
|
||||
* - `/site/templates/fields/fieldName.php`
|
||||
* - `/site/templates/fields/fieldName.templateName.php`
|
||||
* - `/site/templates/fields/fieldName/$file.php` (using $file argument)
|
||||
* - `/site/templates/fields/$file.php` (using $file argument)
|
||||
* - `/site/templates/fields/$file/fieldName.php` (using $file argument, must have trailing slash)
|
||||
* - `/site/templates/fields/$file.fieldName.php` (using $file argument, must have trailing period)
|
||||
* - `/site/templates/fields/fieldName/$file.php`
|
||||
* - `/site/templates/fields/$file.php`
|
||||
* - `/site/templates/fields/$file/fieldName.php`
|
||||
* - `/site/templates/fields/$file.fieldName.php`
|
||||
*
|
||||
* Note that the examples above showing $file require that the `$file` argument is specified.
|
||||
* Note that the examples above showing $file require that the `$file` argument is specified
|
||||
* in the `renderField()` method call.
|
||||
*
|
||||
* ~~~~~
|
||||
* // Render output for the 'images' field (assumes you have implemented an output file)
|
||||
@@ -2948,8 +3040,8 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* #pw-group-output-rendering
|
||||
*
|
||||
* @param string $fieldName May be any custom field name or native page property.
|
||||
* @param string $file Optionally specify file (in site/templates/fields/) to render with (may omit .php extension).
|
||||
* @param mixed|null $value Optionally specify value to render, otherwise it will be pulled from this $page.
|
||||
* @param string $file Optionally specify file (in site/templates/fields/) to render with (may optionally omit .php extension).
|
||||
* @param mixed|null $value Optionally specify value to render, otherwise it will be pulled from this page.
|
||||
* @return mixed|string Returns the rendered value of the field
|
||||
* @see Page::render(), Page::renderValue()
|
||||
*
|
||||
@@ -3000,6 +3092,23 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
*
|
||||
*/
|
||||
public function getInputfields($fieldName = '') {
|
||||
if($this->wire()->hooks->isMethodHooked($this, 'getInputfields')) {
|
||||
return $this->__call('getInputfields', array($fieldName));
|
||||
} else {
|
||||
return $this->___getInputfields($fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hookable version of getInputfields() method.
|
||||
*
|
||||
* See the getInputfields() method above for documentation details.
|
||||
*
|
||||
* @param string|array $fieldName
|
||||
* @return null|InputfieldWrapper Returns an InputfieldWrapper array of Inputfield objects, or NULL on failure.
|
||||
*
|
||||
*/
|
||||
protected function ___getInputfields($fieldName = '') {
|
||||
return $this->values()->getInputfields($this, $fieldName);
|
||||
}
|
||||
|
||||
@@ -3472,7 +3581,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
*
|
||||
* The output formatting state determines if a page's output is allowed to be filtered by runtime formatters.
|
||||
* Pages used for output should have output formatting on. Pages you intend to manipulate and save should
|
||||
* have it off.
|
||||
* have it off. See this post about [output formatting](https://processwire.com/blog/posts/output-formatting/).
|
||||
*
|
||||
* ~~~~~
|
||||
* // Set output formatting state off, for page manipulation
|
||||
@@ -3489,6 +3598,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* @param bool $outputFormatting Optional, default true
|
||||
* @return $this
|
||||
* @see Page::outputFormatting(), Page::of()
|
||||
* @link https://processwire.com/blog/posts/output-formatting/
|
||||
*
|
||||
*/
|
||||
public function setOutputFormatting($outputFormatting = true) {
|
||||
@@ -3519,6 +3629,8 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* - Pages used for front-end output should have output formatting turned ON.
|
||||
*
|
||||
* - Pages that you are manipulating and saving should have output formatting turned OFF.
|
||||
*
|
||||
* See this post about [output formatting](https://processwire.com/blog/posts/output-formatting/).
|
||||
*
|
||||
* ~~~~~
|
||||
* // Set output formatting state off, for page manipulation
|
||||
@@ -3533,6 +3645,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
*
|
||||
* @param bool $outputFormatting If specified, sets output formatting state ON or OFF. If not specified, nothing is changed.
|
||||
* @return bool Current output formatting state (before this function call, if it was changed)
|
||||
* @link https://processwire.com/blog/posts/output-formatting/
|
||||
*
|
||||
*/
|
||||
public function of($outputFormatting = null) {
|
||||
@@ -3985,7 +4098,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
*
|
||||
* ~~~~~
|
||||
* // set and save a meta value
|
||||
* $page->meta()->set('colors', [ 'red, 'green', 'blue' ]);
|
||||
* $page->meta()->set('colors', [ 'red', 'green', 'blue' ]);
|
||||
*
|
||||
* // get a meta value
|
||||
* $colors = $page->meta()->get('colors');
|
||||
@@ -4040,6 +4153,8 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
$this->namePrevious = $old;
|
||||
} else if($what === 'status' && $old !== null) {
|
||||
$this->statusPrevious = (int) $old;
|
||||
} else if($what === 'sort' && $old !== null && $this->sortPrevious === null) {
|
||||
$this->sortPrevious = (int) $old;
|
||||
}
|
||||
}
|
||||
return parent::trackChange($what, $old, $new);
|
||||
|
@@ -28,7 +28,7 @@
|
||||
* ~~~~~
|
||||
* #pw-body
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @method string getMarkup($key = null) Render a simple/default markup value for each item #pw-internal
|
||||
@@ -74,6 +74,16 @@ class PageArray extends PaginatedArray implements WirePaginatable {
|
||||
*/
|
||||
protected $keyIndex = array();
|
||||
|
||||
/**
|
||||
* Construct
|
||||
*
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->indexedByName = false;
|
||||
$this->usesNumericKeys = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template method that descendant classes may use to validate items added to this WireArray
|
||||
*
|
||||
@@ -142,18 +152,6 @@ class PageArray extends PaginatedArray implements WirePaginatable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this PageArray use numeric keys only? (yes it does)
|
||||
*
|
||||
* Defined here to override the slower check in WireArray
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
protected function usesNumericKeys() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per WireArray interface, return a blank Page
|
||||
*
|
||||
@@ -394,7 +392,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
|
||||
*
|
||||
* This is applicable to and destructive to the WireArray.
|
||||
*
|
||||
* @param string|Selectors|array $selectors AttributeSelector string to use as the filter.
|
||||
* @param string|Selectors|array $selectors Selector string to use as the filter.
|
||||
* @param bool|int $not Make this a "not" filter? Use int 1 for "not all". (default is false)
|
||||
* @return PageArray|WireArray reference to current [filtered] PageArray
|
||||
*
|
||||
@@ -409,7 +407,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string $selector AttributeSelector string to use as the filter.
|
||||
* @param string $selector Selector string to use as the filter.
|
||||
* @return PageArray|PaginatedArray|WireArray reference to current PageArray instance.
|
||||
*
|
||||
*/
|
||||
@@ -418,11 +416,11 @@ class PageArray extends PaginatedArray implements WirePaginatable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out pages that don't match the selector (destructive)
|
||||
* Filter out pages that DO match the selector (destructive)
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string $selector AttributeSelector string to use as the filter.
|
||||
* @param string $selector Selector string to use
|
||||
* @return PageArray|PaginatedArray|WireArray reference to current PageArray instance.
|
||||
*
|
||||
*/
|
||||
@@ -454,7 +452,7 @@ class PageArray extends PaginatedArray implements WirePaginatable {
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string $selector AttributeSelector string.
|
||||
* @param string $selector Selector string.
|
||||
* @return PageArray|WireArray New PageArray instance
|
||||
* @see WireArray::find()
|
||||
*
|
||||
@@ -621,10 +619,16 @@ class PageArray extends PaginatedArray implements WirePaginatable {
|
||||
*
|
||||
*/
|
||||
public function __toString() {
|
||||
$s = '';
|
||||
foreach($this as $page) $s .= "$page|";
|
||||
$s = rtrim($s, "|");
|
||||
return $s;
|
||||
$ids = array();
|
||||
if($this->lazyLoad) {
|
||||
$items = $this;
|
||||
} else {
|
||||
$items = &$this->data;
|
||||
}
|
||||
foreach($items as $page) {
|
||||
if(!$page instanceof NullPage) $ids[] = $page->id;
|
||||
}
|
||||
return implode('|', $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -649,7 +653,8 @@ class PageArray extends PaginatedArray implements WirePaginatable {
|
||||
if($out) {
|
||||
$out = "<ul>$out</ul>";
|
||||
if($this->getLimit() && $this->getTotal() > $this->getLimit()) {
|
||||
$pager = $this->wire('modules')->get('MarkupPagerNav');
|
||||
/** @var MarkupPagerNav $pager */
|
||||
$pager = $this->wire()->modules->get('MarkupPagerNav');
|
||||
$out .= $pager->render($this);
|
||||
}
|
||||
}
|
||||
@@ -729,5 +734,3 @@ class PageArray extends PaginatedArray implements WirePaginatable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@@ -177,8 +177,8 @@ class PageComparison {
|
||||
// action is just a string to return
|
||||
$result = $action;
|
||||
}
|
||||
|
||||
} else if(is_callable($action)) {
|
||||
|
||||
} else if(is_callable($action) && (!is_object($action) || $action instanceof \Closure)) {
|
||||
// action is callable
|
||||
$result = call_user_func_array($action, array($val, $key, $page));
|
||||
|
||||
@@ -214,7 +214,7 @@ class PageComparison {
|
||||
// exit early for simple path comparison
|
||||
return true;
|
||||
} else if($page->name === $s) {
|
||||
// early exit for simple name atch
|
||||
// early exit for simple name match
|
||||
return true;
|
||||
} else if(Selectors::stringHasOperator($s)) {
|
||||
// selectors string
|
||||
@@ -243,39 +243,140 @@ class PageComparison {
|
||||
if(!empty($options['useDatabase'])) {
|
||||
$selectors->add(new SelectorEqual('id', $page->id))->add(new SelectorEqual('include', 'all'));
|
||||
return $page->wire()->pages->count($selectors) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
$matches = false;
|
||||
$matchFail = false;
|
||||
$groupSelectors = array(); // same (1) item match group selectors
|
||||
$orGroupSelectors = array(); // OR-group selectors
|
||||
|
||||
foreach($selectors as $selector) {
|
||||
|
||||
$property = $selector->field;
|
||||
$subproperty = '';
|
||||
|
||||
if(is_array($property)) $property = reset($property);
|
||||
if(strpos($property, '.')) list($property, $subproperty) = explode('.', $property, 2);
|
||||
if(in_array($property, $this->matchesIgnores)) continue;
|
||||
|
||||
$matches = true;
|
||||
$value = $page->getUnformatted($property);
|
||||
|
||||
if(is_object($value)) {
|
||||
// convert object to array value(s)
|
||||
$value = $this->getObjectValueArray($value, $subproperty);
|
||||
} else if(is_array($value)) {
|
||||
// ok: selector matches will accept an array
|
||||
} else {
|
||||
// convert to a string value, whatever it may be
|
||||
$value = "$value";
|
||||
if($selector->quote === '(') {
|
||||
// OR-groups are handled below this loop
|
||||
$orGroup = $selector->field() . '.';
|
||||
if(!isset($orGroupSelectors[$orGroup])) $orGroupSelectors[$orGroup] = array();
|
||||
$orGroupSelectors[$orGroup][] = $selector;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!$selector->matches($value)) {
|
||||
$matches = false;
|
||||
if(!$this->selectorMatches($page, $selector)) {
|
||||
$matchFail = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if($selector->group !== null) {
|
||||
// validate that same (1) item matches from these later
|
||||
$groupName = $selector->group;
|
||||
if(!isset($groupSelectors[$groupName])) $groupSelectors[$groupName] = array();
|
||||
$groupSelectors[$groupName][] = $selector;
|
||||
}
|
||||
}
|
||||
|
||||
// OR-groups
|
||||
if(!$matchFail && count($orGroupSelectors)) {
|
||||
foreach($orGroupSelectors as /* $orGroupName => */ $selectors) {
|
||||
$orGroupMatches = false;
|
||||
foreach($selectors as $selector) {
|
||||
if($this->matches($page, $selector->value)) {
|
||||
// OR-group selector matches
|
||||
$orGroupMatches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!$orGroupMatches) {
|
||||
$matchFail = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// same (1) item match groups
|
||||
if(!$matchFail && count($groupSelectors)) {
|
||||
foreach($groupSelectors as /* $groupName => */ $selectors) {
|
||||
$matchGroupKeys = null;
|
||||
foreach($selectors as $selector) {
|
||||
$keys = $selector->get('_matchGroupKeys'); // populated by selectorMatchesProperty
|
||||
if($matchGroupKeys === null) {
|
||||
$matchGroupKeys = $keys;
|
||||
} else {
|
||||
$matchGroupKeys = array_intersect($matchGroupKeys, $keys);
|
||||
}
|
||||
}
|
||||
if(empty($matchGroupKeys)) {
|
||||
$matchFail = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !$matchFail;
|
||||
}
|
||||
|
||||
return $matches;
|
||||
/**
|
||||
* Return whether individual Selector object matches Page
|
||||
*
|
||||
* @param Page $page
|
||||
* @param Selector $selector
|
||||
* @return bool
|
||||
* @since 3.0.231
|
||||
*
|
||||
*/
|
||||
protected function selectorMatches(Page $page, Selector $selector) {
|
||||
$match = false;
|
||||
$properties = $selector->fields();
|
||||
foreach($properties as $property) {
|
||||
$match = $this->selectorMatchesProperty($page, $selector, $property);
|
||||
if($match) break;
|
||||
}
|
||||
return $match;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether single property from individual Selector matches Page
|
||||
*
|
||||
* @param Page $page
|
||||
* @param Selector $selector
|
||||
* @param string $property
|
||||
* @return bool
|
||||
* @since 3.0.231
|
||||
*
|
||||
*/
|
||||
protected function selectorMatchesProperty(Page $page, Selector $selector, $property) {
|
||||
|
||||
$subproperty = '';
|
||||
if(strpos($property, '.')) list($property, $subproperty) = explode('.', $property, 2);
|
||||
if(in_array($property, $this->matchesIgnores)) return true;
|
||||
|
||||
if($selector->quote === '[' && Selectors::stringHasOperator($selector->value())) {
|
||||
$selector->value = $page->wire()->pages->findIDs($selector->value());
|
||||
}
|
||||
|
||||
$value = $page->getUnformatted($property);
|
||||
|
||||
if(is_object($value)) {
|
||||
// convert object to array value(s)
|
||||
$value = $this->getObjectValueArray($value, $subproperty);
|
||||
|
||||
} else if(is_array($value)) {
|
||||
// ok: selector matches will accept an array
|
||||
|
||||
} else {
|
||||
// convert to a string value, whatever it may be
|
||||
$value = "$value";
|
||||
}
|
||||
|
||||
if(!$selector->matches($value)) return false;
|
||||
|
||||
if($selector->group !== null && is_array($value)) {
|
||||
// see which individual values match and record their keys for later comparison
|
||||
$matchGroupKeys = array();
|
||||
foreach($value as $key => $val) {
|
||||
if($selector->matches($val)) $matchGroupKeys[] = $key;
|
||||
}
|
||||
$selector->setQuietly('_matchGroupKeys', $matchGroupKeys); // used by matches() method
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -5,7 +5,7 @@
|
||||
*
|
||||
* Matches selector strings to pages
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* Hookable methods:
|
||||
@@ -1817,6 +1817,7 @@ class PageFinder extends Wire {
|
||||
$tableAlias = $database->escapeTable($tableAlias);
|
||||
|
||||
$join = '';
|
||||
$joinType = '';
|
||||
$numEmptyValues = 0;
|
||||
$valueArray = $selector->values(true);
|
||||
$fieldtype = $field->type;
|
||||
@@ -1835,7 +1836,12 @@ class PageFinder extends Wire {
|
||||
if(in_array($operator, array('=', '!=', '<', '<=', '>', '>='))) {
|
||||
// we only accommodate this optimization for single-value selectors...
|
||||
if($this->whereEmptyValuePossible($field, $subfield, $selector, $query, $value, $whereFields)) {
|
||||
if(count($valueArray) > 1 && $operator == '=') $whereFieldsType = 'OR';
|
||||
if(count($valueArray) > 1) {
|
||||
if($operator == '=') $whereFieldsType = 'OR';
|
||||
} else {
|
||||
$fieldCnt[$field->table]--;
|
||||
if($fieldCnt[$field->table] < 1) unset($fieldCnt[$field->table]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1846,6 +1852,7 @@ class PageFinder extends Wire {
|
||||
$q = $subqueries[$tableAlias];
|
||||
} else {
|
||||
$q = $this->wire(new DatabaseQuerySelect());
|
||||
// $subqueries[$tableAlias] = $q;
|
||||
}
|
||||
|
||||
/** @var PageFinderDatabaseQuerySelect $q */
|
||||
@@ -1855,12 +1862,25 @@ class PageFinder extends Wire {
|
||||
$q->set('selectors', $selectors); // original selectors (all) if required by the fieldtype
|
||||
$q->set('parentQuery', $query);
|
||||
$q->set('pageFinder', $this);
|
||||
$q->set('joinType', $joinType);
|
||||
$q->bindOption('global', true); // ensures bound value key are globally unique
|
||||
$q->bindOption('prefix', 'pf'); // pf=PageFinder
|
||||
|
||||
|
||||
/* @todo To be implemented after 3.0.245
|
||||
if(strpos($subfields, 'JSON.') === 0) {
|
||||
if($this->getMatchQueryJSON($q, $tableAlias, $subfields, $selector->operator, $value)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
$q = $fieldtype->getMatchQuery($q, $tableAlias, $subfield, $selector->operator, $value);
|
||||
$q->copyTo($query, array('select', 'join', 'leftjoin', 'orderby', 'groupby'));
|
||||
$q->copyBindValuesTo($query);
|
||||
|
||||
if($q->joinType && $q->joinType != $joinType) {
|
||||
$joinType = strtolower((string) $q->joinType);
|
||||
}
|
||||
|
||||
if(count($q->where)) {
|
||||
// $and = $selector->not ? "AND NOT" : "AND";
|
||||
@@ -1883,9 +1903,8 @@ class PageFinder extends Wire {
|
||||
}
|
||||
|
||||
if($join) {
|
||||
$joinType = 'join';
|
||||
|
||||
if(count($fields) > 1
|
||||
if($joinType === 'leftjoin'
|
||||
|| count($fields) > 1
|
||||
|| !empty($options['startAfterID']) || !empty($options['stopBeforeID'])
|
||||
|| (count($valueArray) > 1 && $numEmptyValues > 0)
|
||||
|| ($subfield == 'count' && !$this->isRepeaterFieldtype($field->type))
|
||||
@@ -1893,7 +1912,7 @@ class PageFinder extends Wire {
|
||||
|| $selector->operator == '!=') {
|
||||
// join should instead be a leftjoin
|
||||
|
||||
$joinType = "leftjoin";
|
||||
$joinType = 'leftjoin';
|
||||
|
||||
if($where) {
|
||||
$whereType = $lastSelector->str == $selector->str ? "OR" : ") AND (";
|
||||
@@ -1905,6 +1924,8 @@ class PageFinder extends Wire {
|
||||
// removes condition from join, but ensures we still have a $join
|
||||
$join = '1=1';
|
||||
}
|
||||
} else {
|
||||
$joinType = 'join';
|
||||
}
|
||||
|
||||
// we compile the joins after going through all the selectors, so that we can
|
||||
@@ -1973,6 +1994,22 @@ class PageFinder extends Wire {
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get match query when data is stored in a JSON DB column (future use)
|
||||
*
|
||||
* @param PageFinderDatabaseQuerySelect DatabaseQuerySelect $q
|
||||
* @param string $tableAlias
|
||||
* @param string $subfields
|
||||
* @param string $operator
|
||||
* @param string|int|array $value
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
protected function getMatchQueryJSON(DatabaseQuerySelect $q, $tableAlias, $subfields, $operator, $value) {
|
||||
// @todo to be implemented after 3.0.245
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post process a DatabaseQuerySelect for page finder
|
||||
*
|
||||
@@ -2138,7 +2175,9 @@ class PageFinder extends Wire {
|
||||
|
||||
} else if($operator === '!=' || $operator === '<>') {
|
||||
// not equals
|
||||
$whereType = 'AND';
|
||||
$whereType = count($selector->fields()) > 1 && $ft->isEmptyValue($field, $value) ? 'OR' : 'AND';
|
||||
// alternate and technically more consistent behavior, but doesn't seem useful:
|
||||
// $whereType = count($selector->fields()) > 1 ? 'OR' : 'AND';
|
||||
$zeroIsEmpty = $ft->isEmptyValue($field, "0");
|
||||
$zeroIsNotEmpty = !$zeroIsEmpty;
|
||||
$value = (string) $value;
|
||||
@@ -3577,10 +3616,11 @@ class PageFinder extends Wire {
|
||||
if(count($fields) > 1) {
|
||||
// OR fields present
|
||||
array_shift($fields);
|
||||
$subfields = array($subfields);
|
||||
$subfields = array($subfields); // 1. subfields is definitely an array…
|
||||
foreach($fields as $name) {
|
||||
if(strpos($name, "$fieldName.") === 0) {
|
||||
list(,$name) = explode('__owner.', $name);
|
||||
list(,$name) = explode('__owner.', $name);
|
||||
/** @var array $subfields 2. …but PhpStorm in PHP8 mode can't tell it's an array without this */
|
||||
$subfields[] = $name;
|
||||
} else {
|
||||
$this->syntaxError(
|
||||
@@ -3652,7 +3692,7 @@ class PageFinder extends Wire {
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
public function getPageArrayData(PageArray $pageArray = null) {
|
||||
public function getPageArrayData(?PageArray $pageArray = null) {
|
||||
if($pageArray !== null && count($this->pageArrayData)) {
|
||||
$pageArray->data($this->pageArrayData);
|
||||
}
|
||||
@@ -3743,5 +3783,6 @@ class PageFinder extends Wire {
|
||||
* @property Selectors $selectors Original Selectors object
|
||||
* @property DatabaseQuerySelect $parentQuery Parent database query
|
||||
* @property PageFinder $pageFinder PageFinder instance that initiated the query
|
||||
* @property string $joinType Value 'join', 'leftjoin', or '' (if not yet known), can be overridden (3.0.237+)
|
||||
*/
|
||||
abstract class PageFinderDatabaseQuerySelect extends DatabaseQuerySelect { }
|
||||
|
@@ -9,7 +9,7 @@
|
||||
* Except where indicated, please treat these properties as private to the
|
||||
* Page class.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*/
|
||||
@@ -75,6 +75,7 @@ abstract class PageProperties {
|
||||
'addable' => 'm',
|
||||
'child' => 'm',
|
||||
'children' => 'm',
|
||||
'cloneable' => 'm',
|
||||
'created' => 's',
|
||||
'createdStr' => '',
|
||||
'createdUser' => '',
|
||||
@@ -137,6 +138,7 @@ abstract class PageProperties {
|
||||
'rootParent' => 'm',
|
||||
'siblings' => 'm',
|
||||
'sort' => 's',
|
||||
'sortPrevious' => 'p',
|
||||
'sortable' => 'm',
|
||||
'sortfield' => 's',
|
||||
'status' => 's',
|
||||
|
@@ -624,7 +624,7 @@ class PageTraversal {
|
||||
'host' => '',
|
||||
'pageNum' => is_int($options) || (is_string($options) && in_array($options, array('+', '-'))) ? $options : 1,
|
||||
'data' => array(),
|
||||
'urlSegmentStr' => is_string($options) ? $options : '',
|
||||
'urlSegmentStr' => (is_string($options) && !in_array($options, array('+', '-'))) ? $options : '',
|
||||
'urlSegments' => array(),
|
||||
'language' => is_object($options) && wireInstanceOf($options, 'Language') ? $options : null,
|
||||
);
|
||||
@@ -707,7 +707,7 @@ class PageTraversal {
|
||||
}
|
||||
if(!strlen($prefix)) $prefix = $config->pageNumUrlPrefix;
|
||||
$url = rtrim($url, '/') . '/' . $prefix . ((int) $options['pageNum']);
|
||||
if($template->slashPageNum) $url .= '/';
|
||||
if(((int) $template->slashPageNum) === 1) $url .= '/';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,6 +852,7 @@ class PageTraversal {
|
||||
* - `http` (bool): True to force scheme and hostname in URL (default=auto detect).
|
||||
* - `language` (Language|bool): Optionally specify Language to start editor in, or boolean true to force current user language.
|
||||
* - `find` (string): Name of field to find in the editor (3.0.151+)
|
||||
* - `vars` (array): Additional variables to include in query string (3.0.239+)
|
||||
* @return string URL for editing this page
|
||||
*
|
||||
*/
|
||||
@@ -862,6 +863,7 @@ class PageTraversal {
|
||||
$https = $adminTemplate && ($adminTemplate->https > 0) && !$config->noHTTPS;
|
||||
$url = ($https && !$config->https) ? 'https://' . $config->httpHost : '';
|
||||
$url .= $config->urls->admin . "page/edit/?id=$page->id";
|
||||
$optionsArray = is_array($options) ? $options : array();
|
||||
|
||||
if($options === true || (is_array($options) && !empty($options['http']))) {
|
||||
if(strpos($url, '://') === false) {
|
||||
@@ -872,15 +874,22 @@ class PageTraversal {
|
||||
$languages = $page->wire()->languages;
|
||||
if($languages) {
|
||||
$language = $page->wire()->user->language;
|
||||
if(empty($options['language'])) {
|
||||
if(empty($optionsArray['language'])) {
|
||||
if($page->wire()->page->template->id == $adminTemplate->id) $language = null;
|
||||
} else if($options['language'] instanceof Page) {
|
||||
$language = $options['language'];
|
||||
} else if($options['language'] !== true) {
|
||||
$language = $languages->get($options['language']);
|
||||
} else if($optionsArray['language'] instanceof Page) {
|
||||
$language = $optionsArray['language'];
|
||||
} else if($optionsArray['language'] !== true) {
|
||||
$language = $languages->get($optionsArray['language']);
|
||||
}
|
||||
if($language && $language->id) $url .= "&language=$language->id";
|
||||
}
|
||||
|
||||
$version = (int) ((string) $page->get('_version|_repeater_version'));
|
||||
if($version) $url .= "&version=$version";
|
||||
|
||||
if(!empty($optionsArray['vars'])) {
|
||||
$url .= '&' . http_build_query($optionsArray['vars']);
|
||||
}
|
||||
|
||||
$append = $page->wire()->session->getFor($page, 'appendEditUrl');
|
||||
|
||||
@@ -1138,11 +1147,11 @@ class PageTraversal {
|
||||
*
|
||||
* @param Page $page
|
||||
* @param string|array $selector Optional selector. When specified, will find nearest next sibling that matches.
|
||||
* @param PageArray $siblings Optional siblings to use instead of the default. May also be specified as first argument when no selector needed.
|
||||
* @param PageArray|null $siblings Optional siblings to use instead of the default. May also be specified as first argument when no selector needed.
|
||||
* @return Page|NullPage Returns the next sibling page, or a NullPage if none found.
|
||||
*
|
||||
*/
|
||||
public function nextSibling(Page $page, $selector = '', PageArray $siblings = null) {
|
||||
public function nextSibling(Page $page, $selector = '', ?PageArray $siblings = null) {
|
||||
if($selector instanceof PageArray) {
|
||||
// backwards compatible to when $siblings was first argument
|
||||
$siblings = $selector;
|
||||
@@ -1182,11 +1191,11 @@ class PageTraversal {
|
||||
*
|
||||
* @param Page $page
|
||||
* @param string|array $selector Optional selector. When specified, will find nearest previous sibling that matches.
|
||||
* @param PageArray $siblings Optional siblings to use instead of the default. May also be specified as first argument when no selector needed.
|
||||
* @param PageArray|null $siblings Optional siblings to use instead of the default. May also be specified as first argument when no selector needed.
|
||||
* @return Page|NullPage Returns the previous sibling page, or a NullPage if none found.
|
||||
*
|
||||
*/
|
||||
public function prevSibling(Page $page, $selector = '', PageArray $siblings = null) {
|
||||
public function prevSibling(Page $page, $selector = '', ?PageArray $siblings = null) {
|
||||
if($selector instanceof PageArray) {
|
||||
// backwards compatible to when $siblings was first argument
|
||||
$siblings = $selector;
|
||||
@@ -1213,11 +1222,11 @@ class PageTraversal {
|
||||
*
|
||||
* @param Page $page
|
||||
* @param string|array $selector Optional selector. When specified, will filter the found siblings.
|
||||
* @param PageArray $siblings Optional siblings to use instead of the default.
|
||||
* @param PageArray|null $siblings Optional siblings to use instead of the default.
|
||||
* @return PageArray Returns all matching pages after this one.
|
||||
*
|
||||
*/
|
||||
public function nextAllSiblings(Page $page, $selector = '', PageArray $siblings = null) {
|
||||
public function nextAllSiblings(Page $page, $selector = '', ?PageArray $siblings = null) {
|
||||
|
||||
if(is_null($siblings)) {
|
||||
$siblings = $page->parent()->children();
|
||||
@@ -1247,11 +1256,11 @@ class PageTraversal {
|
||||
*
|
||||
* @param Page $page
|
||||
* @param string|array $selector Optional selector. When specified, will filter the found siblings.
|
||||
* @param PageArray $siblings Optional siblings to use instead of the default.
|
||||
* @param PageArray|null $siblings Optional siblings to use instead of the default.
|
||||
* @return PageArray
|
||||
*
|
||||
*/
|
||||
public function prevAllSiblings(Page $page, $selector = '', PageArray $siblings = null) {
|
||||
public function prevAllSiblings(Page $page, $selector = '', ?PageArray $siblings = null) {
|
||||
|
||||
if(is_null($siblings)) {
|
||||
$siblings = $page->parent()->children();
|
||||
@@ -1282,7 +1291,7 @@ class PageTraversal {
|
||||
* @return PageArray
|
||||
*
|
||||
*/
|
||||
public function nextUntilSiblings(Page $page, $selector = '', $filter = '', PageArray $siblings = null) {
|
||||
public function nextUntilSiblings(Page $page, $selector = '', $filter = '', ?PageArray $siblings = null) {
|
||||
|
||||
if(is_null($siblings)) {
|
||||
$siblings = $page->parent()->children();
|
||||
@@ -1334,7 +1343,7 @@ class PageTraversal {
|
||||
* @return PageArray
|
||||
*
|
||||
*/
|
||||
public function prevUntilSiblings(Page $page, $selector = '', $filter = '', PageArray $siblings = null) {
|
||||
public function prevUntilSiblings(Page $page, $selector = '', $filter = '', ?PageArray $siblings = null) {
|
||||
|
||||
if(is_null($siblings)) {
|
||||
$siblings = $page->parent()->children();
|
||||
|
@@ -879,7 +879,7 @@ class PageValues extends Wire {
|
||||
$template = $page->template();
|
||||
if(!$template) return $page->_parentGet($key);
|
||||
|
||||
$field = $this->getField($page, $key);
|
||||
$field = $page->getField($key);
|
||||
$value = $page->_parentGet($key);
|
||||
|
||||
if(!$field) return $value; // likely a runtime field, not part of our data
|
||||
@@ -1021,7 +1021,16 @@ class PageValues extends Wire {
|
||||
public function setFieldValue(Page $page, $key, $value, $load = true) {
|
||||
|
||||
if(!$page->template()) {
|
||||
throw new WireException("You must assign a template to the page before setting field values ($key)");
|
||||
$config = $page->wire()->config;
|
||||
$name = strpos($key, '__') ? substr($key, 0, strpos($key, '__')) : $key;
|
||||
$error = "You must assign a template to page $page before setting '$name' field.";
|
||||
if($config->debug) {
|
||||
// allow page to proceed in debug mode so that it's possible to delete it if needed
|
||||
$page->error($error);
|
||||
$page->template($page->wire()->pages->get($config->http404PageID)->template);
|
||||
} else {
|
||||
throw new WireException($error);
|
||||
}
|
||||
}
|
||||
|
||||
$isLoaded = $page->isLoaded();
|
||||
@@ -1038,7 +1047,7 @@ class PageValues extends Wire {
|
||||
}
|
||||
|
||||
// check if the given key resolves to a Field or not
|
||||
$field = $this->getField($page, $key);
|
||||
$field = $page->getField($key);
|
||||
if(!$field) {
|
||||
// not a known/saveable field, let them use it for runtime storage
|
||||
$valPrevious = $page->_parentGet($key);
|
||||
|
@@ -12,7 +12,7 @@
|
||||
* Pagefile objects are contained by a `Pagefiles` object.
|
||||
* #pw-body
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @property-read string $url URL to the file on the server.
|
||||
@@ -357,7 +357,7 @@ class Pagefile extends WireData implements WireArrayItem {
|
||||
$key = $type === 'created' ? '_createdUser' : '_modifiedUser';
|
||||
if(!$this->$key) {
|
||||
$id = (int) parent::get($type . '_users_id');
|
||||
$this->$key = $id ? $this->wire('users')->get($id) : new NullPage();
|
||||
$this->$key = ($id ? $this->wire()->users->get($id) : new NullPage());
|
||||
}
|
||||
return $this->$key;
|
||||
}
|
||||
@@ -421,11 +421,11 @@ class Pagefile extends WireData implements WireArrayItem {
|
||||
* Set a description, optionally parsing JSON language-specific descriptions to separate properties
|
||||
*
|
||||
* @param string|array $value
|
||||
* @param Page|Language Langage to set it for. Omit to determine automatically.
|
||||
* @param Language|null Langage to set it for. Omit to determine automatically.
|
||||
* @return $this
|
||||
*
|
||||
*/
|
||||
protected function setDescription($value, Page $language = null) {
|
||||
protected function setDescription($value, ?Page $language = null) {
|
||||
|
||||
$languages = $this->wire()->languages;
|
||||
|
||||
@@ -577,7 +577,7 @@ class Pagefile extends WireData implements WireArrayItem {
|
||||
|
||||
if(is_null($language)) {
|
||||
// return description for current user language, or inherit from default if not available
|
||||
$user = $this->wire('user');
|
||||
$user = $this->wire()->user;
|
||||
$value = null;
|
||||
if($user->language && $user->language->id) {
|
||||
$value = parent::get("description{$user->language}");
|
||||
@@ -707,6 +707,7 @@ class Pagefile extends WireData implements WireArrayItem {
|
||||
$value = $this->uploadName();
|
||||
break;
|
||||
default:
|
||||
if(strpos($key, '|')) return parent::get($key);
|
||||
$value = $this->getFieldValue($key);
|
||||
}
|
||||
|
||||
@@ -1386,12 +1387,12 @@ class Pagefile extends WireData implements WireArrayItem {
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string $name
|
||||
* @param PagefileExtra $value
|
||||
* @param PagefileExtra|null $value
|
||||
* @return PagefileExtra[]|PagefileExtra|null
|
||||
* @since 3.0.132
|
||||
*
|
||||
*/
|
||||
public function extras($name = null, PagefileExtra $value = null) {
|
||||
public function extras($name = null, ?PagefileExtra $value = null) {
|
||||
if($name === null) return $this->extras;
|
||||
if($value instanceof PagefileExtra) {
|
||||
$this->extras[$name] = $value;
|
||||
@@ -1459,6 +1460,45 @@ class Pagefile extends WireData implements WireArrayItem {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all filenames associated with this file
|
||||
*
|
||||
* @return array
|
||||
* @since 3.0.233
|
||||
*
|
||||
*/
|
||||
public function getFiles() {
|
||||
$filename = $this->filename();
|
||||
$filenames = array($filename);
|
||||
foreach($this->extras() as $extra) {
|
||||
if($extra->exists()) $filenames[] = $extra->filename();
|
||||
}
|
||||
return $filenames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set hidden state of this file
|
||||
*
|
||||
* Files that are hidden do not appear in the formatted field value,
|
||||
* but do appear in the unformatted value.
|
||||
*
|
||||
* @param bool|null $set
|
||||
* @since 3.0.237
|
||||
*
|
||||
*/
|
||||
public function hidden($set = null) {
|
||||
$value = (bool) $this->filedata('_hide');
|
||||
if($set === null || $set === $value) return $value;
|
||||
if($set === false) {
|
||||
$this->filedata(false, '_hide');
|
||||
} else if($set === true) {
|
||||
$this->filedata('_hide', true);
|
||||
} else {
|
||||
throw new WireException('Invalid arg for Pagefile::hidden(arg)');
|
||||
}
|
||||
return $set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that isset() and empty() work for dynamic class properties
|
||||
|
@@ -38,7 +38,7 @@
|
||||
* Typically a Pagefiles object will be associated with a specific field attached to a Page.
|
||||
* There may be multiple instances of Pagefiles attached to a given Page (depending on what fields are in it's fieldgroup).
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*
|
||||
@@ -124,6 +124,8 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
|
||||
public function __construct(Page $page) {
|
||||
$this->setPage($page);
|
||||
parent::__construct();
|
||||
$this->usesNumericKeys = false;
|
||||
$this->indexedByName = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,13 +306,13 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
|
||||
/**
|
||||
* Get for direct access to properties
|
||||
*
|
||||
* @param int|string $property
|
||||
* @param int|string $name
|
||||
* @return bool|mixed|Page|Wire|WireData
|
||||
*
|
||||
*/
|
||||
public function __get($property) {
|
||||
if(in_array($property, array('page', 'field', 'url', 'path'))) return $this->get($property);
|
||||
return parent::__get($property);
|
||||
public function __get($name) {
|
||||
if(in_array($name, array('page', 'field', 'url', 'path'))) return $this->get($name);
|
||||
return parent::__get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -375,7 +377,9 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
|
||||
*/
|
||||
public function hookPageSave() {
|
||||
|
||||
if($this->page && $this->field && !$this->page->isChanged($this->field->name)) return $this;
|
||||
if($this->page && $this->field) {
|
||||
if(!$this->page->isChanged($this->field->name)) return $this;
|
||||
}
|
||||
|
||||
$this->page->filesManager()->uncache();
|
||||
|
||||
@@ -595,6 +599,7 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
|
||||
}
|
||||
}
|
||||
|
||||
$basename = strtolower($basename);
|
||||
if(!ctype_alnum(ltrim($ext, '.'))) $ext = preg_replace('/[^a-z0-9.]/', '_', $ext);
|
||||
if(!$allowDots && strpos($basename, '.') !== false) $basename = str_replace('.', '_', $basename);
|
||||
$basename .= $ext;
|
||||
@@ -780,12 +785,13 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
|
||||
if(!is_bool($set)) {
|
||||
// temp status is not being set
|
||||
if(!$isTemp) return false; // if not a temp file, we can exit now
|
||||
if(!$checkDeletable) return $isTemp; // if not checking deletable, we can exit now
|
||||
if(!$checkDeletable) return true; // if not checking deletable, we can exit now
|
||||
}
|
||||
|
||||
$user = $this->wire('user');
|
||||
$user = $this->wire()->user;
|
||||
$session = $this->wire()->session;
|
||||
|
||||
$now = time();
|
||||
$session = $this->wire('session');
|
||||
$pageID = $this->page ? $this->page->id : 0;
|
||||
$fieldID = $this->field ? $this->field->id : 0;
|
||||
$sessionKey = "tempFiles_{$pageID}_{$fieldID}";
|
||||
@@ -806,8 +812,11 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
|
||||
unset($tempFiles[$pagefile->basename]);
|
||||
// remove file from session - note that this means a 'deletable' check can only be used once, for newly uploaded files
|
||||
// as it is assumed you will be removing the file as a result of this method call
|
||||
if(count($tempFiles)) $session->set($this, $sessionKey, $tempFiles);
|
||||
else $session->remove($this, $sessionKey);
|
||||
if(count($tempFiles)) {
|
||||
$session->set($this, $sessionKey, $tempFiles);
|
||||
} else {
|
||||
$session->remove($this, $sessionKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -867,7 +876,11 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
|
||||
}
|
||||
if(count($removed) && $this->page && $this->field) {
|
||||
$this->page->save($this->field->name, array('quiet' => true));
|
||||
$this->message("Removed '{$this->field->name}' temp file(s) for page {$this->page->path} - " . implode(', ', $removed), Notice::debug | Notice::log);
|
||||
$this->message(
|
||||
"Removed '{$this->field->name}' temp file(s) for page {$this->page->path} - " .
|
||||
implode(', ', $removed),
|
||||
Notice::debug | Notice::log
|
||||
);
|
||||
}
|
||||
return count($removed);
|
||||
}
|
||||
@@ -979,6 +992,22 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
|
||||
return $fieldtype->getFieldsPage($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all filenames associated with this Pagefiles object
|
||||
*
|
||||
* @return array
|
||||
* @since 3.0.233
|
||||
*
|
||||
*/
|
||||
public function getFiles() {
|
||||
$filenames = array();
|
||||
foreach($this as $pagefile) {
|
||||
/** @var Pagefile $pagefile */
|
||||
$filenames = array_merge($filenames, $pagefile->getFiles());
|
||||
}
|
||||
return $filenames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug info
|
||||
*
|
||||
|
@@ -154,7 +154,7 @@ class Pageimage extends Pagefile {
|
||||
* $pageimage = new Pageimage($page->images, '/path/to/file.png');
|
||||
* ~~~~~
|
||||
*
|
||||
* @param Pageimages|Pagefiles $pagefiles
|
||||
* @param Pagefiles $pagefiles
|
||||
* @param string $filename Full path and filename to this pagefile
|
||||
* @throws WireException
|
||||
*
|
||||
@@ -543,10 +543,17 @@ class Pageimage extends Pagefile {
|
||||
if(!$filename) $filename = $this->filename;
|
||||
$xml = @file_get_contents($filename);
|
||||
|
||||
if($xml) {
|
||||
$a = @simplexml_load_string($xml)->attributes();
|
||||
if($xml && false !== ($a = @simplexml_load_string($xml))) {
|
||||
$a = $a->attributes();
|
||||
if((int) $a->width > 0) $width = (int) $a->width;
|
||||
if((int) $a->height > 0) $height = (int) $a->height;
|
||||
if((!$width || !$height) && $a->viewBox) {
|
||||
$values = explode(' ', $a->viewBox);
|
||||
if(count($values) === 4) {
|
||||
$width = (int) round($values[2]);
|
||||
$height = (int) round($values[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if((!$width || !$height) && (extension_loaded('imagick') || class_exists('\IMagick'))) {
|
||||
@@ -1702,17 +1709,26 @@ class Pageimage extends Pagefile {
|
||||
/**
|
||||
* Get WebP "extra" version of this Pageimage
|
||||
*
|
||||
* @param array $webpOptions Optionally override certain defaults from `$config->webpOptions` (requires 3.0.229+):
|
||||
* - `useSrcUrlOnSize` (bool): Fallback to source file URL when webp file is larger than source? (default=true)
|
||||
* - `useSrcUrlOnFail` (bool): Fallback to source file URL when webp file fails for some reason? (default=true)
|
||||
* - `quality' (int): Quality setting of 1-100 where higher is better but larger in file size (default=90)
|
||||
* Note that his quality setting is only used if the .webp file does not already exist.
|
||||
* @return PagefileExtra
|
||||
* @since 3.0.132
|
||||
*
|
||||
*/
|
||||
public function webp() {
|
||||
public function webp(array $webpOptions = array()) {
|
||||
$webp = $this->extras('webp');
|
||||
if(!$webp) {
|
||||
$webp = new PagefileExtra($this, 'webp');
|
||||
$webp->setArray($this->wire()->config->webpOptions);
|
||||
$webpOptions = array_merge($this->wire()->config->webpOptions, $webpOptions);
|
||||
$webp->setArray($webpOptions);
|
||||
$this->extras('webp', $webp);
|
||||
$webp->addHookAfter('create', $this, 'hookWebpCreate');
|
||||
} else if(count($webpOptions)) {
|
||||
/** @var PagefileExtra $webp */
|
||||
$webp->setArray($webpOptions);
|
||||
}
|
||||
return $webp;
|
||||
}
|
||||
@@ -1747,6 +1763,8 @@ class Pageimage extends Pagefile {
|
||||
$width = $this->width;
|
||||
$height = 0;
|
||||
}
|
||||
$quality = (int) $webp->get('quality');
|
||||
if($quality > 0) $options['webpQuality'] = $quality;
|
||||
$options['webpAdd'] = true;
|
||||
try {
|
||||
$original->size($width, $height, $options);
|
||||
@@ -1764,12 +1782,12 @@ class Pageimage extends Pagefile {
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string $name
|
||||
* @param PagefileExtra $value
|
||||
* @param PagefileExtra|null $value
|
||||
* @return PagefileExtra[]
|
||||
* @since 3.0.132
|
||||
*
|
||||
*/
|
||||
public function extras($name = null, PagefileExtra $value = null) {
|
||||
public function extras($name = null, ?PagefileExtra $value = null) {
|
||||
if($name) return parent::extras($name, $value);
|
||||
$extras = parent::extras();
|
||||
$extras['webp'] = $this->webp();
|
||||
@@ -1834,6 +1852,28 @@ class Pageimage extends Pagefile {
|
||||
return parent::__isset($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all filenames associated with this image
|
||||
*
|
||||
* @return array
|
||||
* @since 3.0.233
|
||||
*
|
||||
*/
|
||||
public function getFiles() {
|
||||
$filenames = parent::getFiles();
|
||||
foreach($this->extras() as $extra) {
|
||||
if($extra->exists()) $filenames[] = $extra->filename();
|
||||
}
|
||||
foreach($this->getVariations() as $pagefile) {
|
||||
/** @var Pagefile $pagefile */
|
||||
$filenames[] = $pagefile->filename();
|
||||
foreach($pagefile->extras() as $extra) {
|
||||
if($extra->exists()) $filenames[] = $extra->filename();
|
||||
}
|
||||
}
|
||||
return $filenames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic debug info
|
||||
*
|
||||
@@ -1873,4 +1913,3 @@ class Pageimage extends Pagefile {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,7 @@
|
||||
*
|
||||
* This is the most used object in the ProcessWire API.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @link http://processwire.com/api/variables/pages/ Offical $pages Documentation
|
||||
@@ -39,8 +39,9 @@
|
||||
* HOOKABLE METHODS
|
||||
* ================
|
||||
* @method PageArray find($selectorString, array $options = array()) Find and return all pages matching the given selector string. Returns a PageArray. #pw-group-retrieval
|
||||
* @method bool save(Page $page, $options = array()) Save any changes made to the given $page. Same as : $page->save() Returns true on success. #pw-group-manipulation
|
||||
* @method bool save(Page $page, $options = array()) Save any changes made to the given $page. Same as $page->save(); Returns true on success. #pw-group-manipulation
|
||||
* @method bool saveField(Page $page, $field, array $options = array()) Save just the named field from $page. Same as: $page->save('field') #pw-group-manipulation
|
||||
* @method array saveFields(Page $page, $fields, array $options = array()) Saved multiple named fields for $page. @since 3.0.242 #pw-group-manipulation
|
||||
* @method bool trash(Page $page, $save = true) Move a page to the trash. If you have already set the parent to somewhere in the trash, then this method won't attempt to set it again. #pw-group-manipulation
|
||||
* @method bool restore(Page $page, $save = true) Restore a trashed page to its original location. #pw-group-manipulation
|
||||
* @method int|array emptyTrash(array $options = array()) Empty the trash and return number of pages deleted. #pw-group-manipulation
|
||||
@@ -62,10 +63,12 @@
|
||||
* @method saveReady(Page $page) Hook called just before a page is saved.
|
||||
* @method saved(Page $page, array $changes = array(), $values = array()) Hook called after a page is successfully saved.
|
||||
* @method added(Page $page) Hook called when a new page has been added.
|
||||
* @method moveReady(Page $page) Hook called when a page is about to be moved to another parent.
|
||||
* @method moved(Page $page) Hook called when a page has been moved from one parent to another.
|
||||
* @method templateChanged(Page $page) Hook called when a page template has been changed.
|
||||
* @method trashReady(Page $page) Hook called when a page is about to be moved to the trash.
|
||||
* @method trashed(Page $page) Hook called when a page has been moved to the trash.
|
||||
* @method restoreReady(Page $page) Hook called when a page is about to be restored out of the trash.
|
||||
* @method restored(Page $page) Hook called when a page has been moved OUT of the trash.
|
||||
* @method deleteReady(Page $page, array $options) Hook called just before a page is deleted.
|
||||
* @method deleted(Page $page, array $options) Hook called after a page has been deleted.
|
||||
@@ -73,6 +76,7 @@
|
||||
* @method deletedBranch(Page $page, array $options, $numDeleted) Hook called after branch of pages deleted, on initiating page only.
|
||||
* @method cloneReady(Page $page, Page $copy) Hook called just before a page is cloned.
|
||||
* @method cloned(Page $page, Page $copy) Hook called after a page has been successfully cloned.
|
||||
* @method renameReady(Page $page) Hook called when a page is about to be renamed.
|
||||
* @method renamed(Page $page) Hook called after a page has been successfully renamed.
|
||||
* @method sorted(Page $page, $children = false, $total = 0) Hook called after $page has been sorted.
|
||||
* @method statusChangeReady(Page $page) Hook called when a page's status has changed and is about to be saved.
|
||||
@@ -87,9 +91,6 @@
|
||||
* @method savedPageOrField(Page $page, array $changes) Hook inclusive of both saved() and savedField().
|
||||
* @method found(PageArray $pages, array $details) Hook called at the end of a $pages->find().
|
||||
*
|
||||
* TO-DO
|
||||
* =====
|
||||
* @todo Update saveField to accept array of field names as an option.
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -263,7 +264,7 @@ class Pages extends Wire {
|
||||
* - `findOne` (bool): Apply optimizations for finding a single page (default=false).
|
||||
* - `findAll` (bool): Find all pages with no exclusions, same as "include=all" option (default=false).
|
||||
* - `findIDs` (bool|int): 1 to get array of page IDs, true to return verbose array, 2 to return verbose array with all cols in 3.0.153+. (default=false).
|
||||
* - `getTotal` (bool): Whether to set returning PageArray's "total" property (default=true, except when findOne=true).
|
||||
* - `getTotal` (bool): Whether to set returning PageArray's "total" property (default=true) except when findOne=true.
|
||||
* - `loadPages` (bool): Whether to populate the returned PageArray with found pages (default=true).
|
||||
* The only reason why you'd want to change this to false would be if you only needed the count details from
|
||||
* the PageArray: getTotal(), getStart(), getLimit, etc. This is intended as an optimization for $pages->count().
|
||||
@@ -756,8 +757,8 @@ class Pages extends Wire {
|
||||
}
|
||||
|
||||
if(!empty($options['parent_id'])) {
|
||||
unset($options['parent_id']);
|
||||
$parent_id = (int) $options['parent_id'];
|
||||
unset($options['parent_id']);
|
||||
} else if($parent) {
|
||||
unset($options['parent']);
|
||||
if($parent instanceof Page) {
|
||||
@@ -867,6 +868,38 @@ class Pages extends Wire {
|
||||
return $this->editor()->saveField($page, $field, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save multiple named fields from given page
|
||||
*
|
||||
* ~~~~~
|
||||
* // you can specify field names as array…
|
||||
* $a = $pages->saveFields($page, [ 'title', 'body', 'summary' ]);
|
||||
*
|
||||
* // …or a CSV string of field names:
|
||||
* $a = $pages->saveFields($page, 'title, body, summary');
|
||||
*
|
||||
* // return value is array of saved field/property names
|
||||
* print_r($a); // outputs: array( 'title', 'body', 'summary' )
|
||||
* ~~~~~
|
||||
*
|
||||
* #pw-group-manipulation
|
||||
*
|
||||
* @param Page $page Page to save
|
||||
* @param array|string|string[]|Field[] $fields Array of field names to save or CSV/space separated field names to save.
|
||||
* These should only be Field names and not native page property names.
|
||||
* @param array|string $options Optionally specify one or more of the following to modify default behavior:
|
||||
* - `quiet` (boolean): Specify true to bypass updating of modified user and time (default=false).
|
||||
* - `noHooks` (boolean): Prevent before/after save hooks (default=false), please also use $pages->___saveField() for call.
|
||||
* - See $options argument for Pages::save() for additional options
|
||||
* @return array Array of saved field names (may also include property names if they were modified)
|
||||
* @throws WireException
|
||||
* @since 3.0.242
|
||||
*
|
||||
*/
|
||||
public function ___saveFields(Page $page, $fields, array $options = array()) {
|
||||
return $this->editor()->saveFields($page, $fields, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new page using the given template and parent
|
||||
*
|
||||
@@ -1013,7 +1046,7 @@ class Pages extends Wire {
|
||||
* @throws WireException|\Exception on fatal error
|
||||
*
|
||||
*/
|
||||
public function ___clone(Page $page, Page $parent = null, $recursive = true, $options = array()) {
|
||||
public function ___clone(Page $page, ?Page $parent = null, $recursive = true, $options = array()) {
|
||||
return $this->editor()->_clone($page, $parent, $recursive, $options);
|
||||
}
|
||||
|
||||
@@ -1395,6 +1428,7 @@ class Pages extends Wire {
|
||||
* - Assigns a 'sort' value'.
|
||||
*
|
||||
* #pw-internal
|
||||
* #pw-group-manipulation
|
||||
*
|
||||
* @param Page $page
|
||||
*
|
||||
@@ -1412,6 +1446,7 @@ class Pages extends Wire {
|
||||
* already have a name, unless the name is "untitled"
|
||||
*
|
||||
* #pw-internal
|
||||
* #pw-group-manipulation
|
||||
*
|
||||
* @param Page $page
|
||||
* @param array $options
|
||||
@@ -1655,13 +1690,13 @@ class Pages extends Wire {
|
||||
*
|
||||
* #pw-group-cache
|
||||
*
|
||||
* @param Page $page Optional Page that initiated the uncacheAll
|
||||
* @param Page|null $page Optional Page that initiated the uncacheAll
|
||||
* @param array $options Options to modify default behavior:
|
||||
* - `shallow` (bool): By default, this method also calls $page->uncache(). To prevent that call, set this to true.
|
||||
* @return int Number of pages uncached
|
||||
*
|
||||
*/
|
||||
public function uncacheAll(Page $page = null, array $options = array()) {
|
||||
public function uncacheAll(?Page $page = null, array $options = array()) {
|
||||
return $this->cacher->uncacheAll($page, $options);
|
||||
}
|
||||
|
||||
@@ -1915,6 +1950,11 @@ class Pages extends Wire {
|
||||
$class = empty($options['pageClass']) ? 'Page' : $options['pageClass'];
|
||||
|
||||
unset($options['template'], $options['parent'], $options['pageClass']);
|
||||
|
||||
if($template && !$template instanceof Template) {
|
||||
$template = $this->wire()->templates->get($template);
|
||||
if(!$template instanceof Template) $template = null;
|
||||
}
|
||||
|
||||
if(strpos($class, "\\") === false) $class = wireClassName($class, true);
|
||||
|
||||
@@ -2198,6 +2238,20 @@ class Pages extends Wire {
|
||||
$page->setQuietly('_added', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook called when a page is about to be moved to another parent
|
||||
*
|
||||
* Note the previous parent is accessible in the `$page->parentPrevious` property.
|
||||
*
|
||||
* #pw-hooker
|
||||
*
|
||||
* @param Page $page Page that is about to be moved.
|
||||
* @since 3.0.235
|
||||
*
|
||||
*/
|
||||
public function ___moveReady(Page $page) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook called when a page has been moved from one parent to another
|
||||
*
|
||||
@@ -2258,6 +2312,18 @@ class Pages extends Wire {
|
||||
public function ___trashed(Page $page) {
|
||||
$this->log("Trashed page", $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook called when a page is about to be moved OUT of the trash (restored)
|
||||
*
|
||||
* #pw-hooker
|
||||
*
|
||||
* @param Page $page Page that is about to be restored
|
||||
* @since 3.0.235
|
||||
*
|
||||
*/
|
||||
public function ___restoreReady(Page $page) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook called when a page has been moved OUT of the trash (restored)
|
||||
@@ -2387,6 +2453,29 @@ class Pages extends Wire {
|
||||
public function ___cloned(Page $page, Page $copy) {
|
||||
$this->log("Cloned page to $copy->path", $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook called when a page is about to be renamed i.e. had its name field change)
|
||||
*
|
||||
* The previous name can be accessed at `$page->namePrevious`.
|
||||
* The new name can be accessed at `$page->name`.
|
||||
*
|
||||
* This hook is only called when a page's name changes. It is not called when
|
||||
* a page is moved unless the name was changed at the same time.
|
||||
*
|
||||
* **Multi-language note:**
|
||||
* Also note this hook may be called if a page's multi-language name changes.
|
||||
* In those cases the language-specific name is stored in "name123" while the
|
||||
* previous value is stored in "-name123" (where 123 is the language ID).
|
||||
*
|
||||
* #pw-hooker
|
||||
*
|
||||
* @param Page $page The $page that was renamed
|
||||
* @since 3.0.235
|
||||
*
|
||||
*/
|
||||
public function ___renameReady(Page $page) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook called when a page has been renamed (i.e. had its name field change)
|
||||
|
@@ -5,7 +5,7 @@
|
||||
*
|
||||
* Implements page manipulation methods of the $pages API variable
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*/
|
||||
@@ -27,7 +27,7 @@ class PagesEditor extends Wire {
|
||||
*
|
||||
*/
|
||||
protected $pages;
|
||||
|
||||
|
||||
/**
|
||||
* Construct
|
||||
*
|
||||
@@ -353,7 +353,7 @@ class PagesEditor extends Wire {
|
||||
if(!$parent->id) $parent = $this->pages->get("include=all, template=$idStr");
|
||||
}
|
||||
|
||||
if($parent->id) $page->parent = $parent;
|
||||
if($parent && $parent->id) $page->parent = $parent;
|
||||
}
|
||||
|
||||
// assign page name
|
||||
@@ -424,6 +424,9 @@ class PagesEditor extends Wire {
|
||||
* - `ignoreFamily` (boolean): Bypass check of allowed family/parent settings when saving (default=false)
|
||||
* - `noHooks` (boolean): Prevent before/after save hooks from being called (default=false)
|
||||
* - `noFields` (boolean): Bypass saving of custom fields (default=false)
|
||||
* - `caller` (string): Optional name of calling function (i.e. 'pages.trash'), for internal use (default='') 3.0.235+
|
||||
* - `callback` (string|callable): Hook method name from $pages or callable to trigger after save.
|
||||
* It receives a single $page argument. For internal use. (default='') 3.0.235+
|
||||
* @return bool True on success, false on failure
|
||||
* @throws WireException
|
||||
*
|
||||
@@ -438,6 +441,8 @@ class PagesEditor extends Wire {
|
||||
'ignoreFamily' => false,
|
||||
'noHooks' => false,
|
||||
'noFields' => false,
|
||||
'caller' => '',
|
||||
'callback' => '',
|
||||
);
|
||||
|
||||
if(is_string($options)) $options = Selectors::keyValueStringToArray($options);
|
||||
@@ -445,11 +450,15 @@ class PagesEditor extends Wire {
|
||||
$user = $this->wire()->user;
|
||||
$languages = $this->wire()->languages;
|
||||
$language = null;
|
||||
$parentPrevious = $page->parentPrevious;
|
||||
$caller = $options['caller'];
|
||||
$callback = $options['callback'];
|
||||
$useHooks = empty($options['noHooks']);
|
||||
|
||||
// if language support active, switch to default language so that saved fields and hooks don't need to be aware of language
|
||||
if($languages && $page->id != $user->id) {
|
||||
$language = $user->language && $user->language->id ? $user->language : null;
|
||||
if($language) $user->language = $languages->getDefault();
|
||||
if($languages && $page->id != $user->id && "$user->language") {
|
||||
$language = $user->language;
|
||||
$user->setLanguage($languages->getDefault());
|
||||
}
|
||||
|
||||
$reason = '';
|
||||
@@ -457,26 +466,42 @@ class PagesEditor extends Wire {
|
||||
if($isNew) $this->pages->setupNew($page);
|
||||
|
||||
if(!$this->isSaveable($page, $reason, '', $options)) {
|
||||
if($language) $user->language = $language;
|
||||
throw new WireException("Can’t save page {$page->id}: {$page->path}: $reason");
|
||||
if($language) $user->setLanguage($language);
|
||||
throw new WireException(rtrim("Can’t save page (id=$page->id): $page->path", ": ") . ": $reason");
|
||||
}
|
||||
|
||||
if($page->hasStatus(Page::statusUnpublished) && $page->template->noUnpublish) {
|
||||
$page->removeStatus(Page::statusUnpublished);
|
||||
}
|
||||
|
||||
if($page->parentPrevious && !$isNew) {
|
||||
if($page->isTrash() && !$page->parentPrevious->isTrash()) {
|
||||
$this->pages->trash($page, false);
|
||||
} else if($page->parentPrevious->isTrash() && !$page->parent->isTrash()) {
|
||||
$this->pages->restore($page, false);
|
||||
if($parentPrevious && !$isNew) {
|
||||
if($useHooks) $this->pages->moveReady($page);
|
||||
if($caller !== 'pages.trash' && $caller !== 'pages.restore') {
|
||||
if($page->isTrash() && !$parentPrevious->isTrash()) {
|
||||
if($this->pages->trash($page, false)) $callback = 'trashed';
|
||||
} else if($parentPrevious->isTrash() && !$page->parent->isTrash()) {
|
||||
if($this->pages->restore($page, false)) $callback = 'restored';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($options['adjustName']) $this->pages->names()->checkNameConflicts($page);
|
||||
if(!$this->savePageQuery($page, $options)) return false;
|
||||
$result = $this->savePageFinish($page, $isNew, $options);
|
||||
if($language) $user->language = $language; // restore language
|
||||
|
||||
if($page->namePrevious && !$isNew && $page->namePrevious != $page->name) {
|
||||
if($useHooks) $this->pages->renameReady($page);
|
||||
}
|
||||
|
||||
$result = $this->savePageQuery($page, $options);
|
||||
if($result) $result = $this->savePageFinish($page, $isNew, $options);
|
||||
if($language) $user->setLanguage($language); // restore language
|
||||
|
||||
if($result && !empty($callback) && $useHooks) {
|
||||
if(is_string($callback) && ctype_alnum($callback)) {
|
||||
$this->pages->$callback($page); // hook method name in $pages
|
||||
} else if(is_callable($callback)) {
|
||||
$callback($page); // user defined callback
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
@@ -790,6 +815,11 @@ class PagesEditor extends Wire {
|
||||
if($page->templatePrevious) $this->pages->templateChanged($page);
|
||||
if(in_array('status', $changes)) $this->pages->statusChanged($page);
|
||||
}
|
||||
|
||||
if($triggerAddedPage && $page->rootParent()->id === $this->wire()->config->trashPageID) {
|
||||
// new page created directly in trash, not a great way to start but that's how it is
|
||||
$this->savePageStatus($page, Page::statusTrash);
|
||||
}
|
||||
|
||||
$this->pages->debugLog('save', $page, true);
|
||||
|
||||
@@ -912,6 +942,90 @@ class PagesEditor extends Wire {
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save multiple named fields from given page
|
||||
*
|
||||
* ~~~~~
|
||||
* // you can specify field names as array…
|
||||
* $a = $pages->saveFields($page, [ 'title', 'body', 'summary' ]);
|
||||
*
|
||||
* // …or a CSV string of field names:
|
||||
* $a = $pages->saveFields($page, 'title, body, summary');
|
||||
*
|
||||
* // return value is array of saved field/property names
|
||||
* print_r($a); // outputs: array( 'title', 'body', 'summary' )
|
||||
* ~~~~~
|
||||
*
|
||||
* @param Page $page Page to save
|
||||
* @param array|string|string[]|Field[] $fields Array of field names to save or CSV/space separated field names to save.
|
||||
* These should only be Field names and not native page property names.
|
||||
* @param array|string $options Optionally specify one or more of the following to modify default behavior:
|
||||
* - `quiet` (boolean): Specify true to bypass updating of modified user and time (default=false).
|
||||
* - `noHooks` (boolean): Prevent before/after save hooks (default=false), please also use $pages->___saveField() for call.
|
||||
* - See $options argument for Pages::save() for additional options
|
||||
* @return array Array of saved field names (may also include property names if they were modified)
|
||||
* @throws WireException
|
||||
* @since 3.0.242
|
||||
*
|
||||
*/
|
||||
public function saveFields(Page $page, $fields, array $options = array()) {
|
||||
|
||||
$saved = array();
|
||||
$quiet = !empty($options['quiet']);
|
||||
$noHooks = !empty($options['noHooks']);
|
||||
|
||||
// do not update modified user/time until last save
|
||||
if(!$quiet) $options['quiet'] = true;
|
||||
|
||||
if(!is_array($fields)) {
|
||||
$fields = explode(' ', str_replace(',', ' ', "$fields"));
|
||||
}
|
||||
|
||||
foreach($fields as $key => $field) {
|
||||
$field = trim("$field");
|
||||
if(empty($field) || !$page->hasField($field)) unset($fields[$key]);
|
||||
}
|
||||
|
||||
// save each field
|
||||
foreach($fields as $field) {
|
||||
if($noHooks) {
|
||||
$success = $this->saveField($page, $field, $options);
|
||||
} else {
|
||||
$success = $this->pages->saveField($page, $field, $options);
|
||||
}
|
||||
if($success) {
|
||||
$saved[$field] = $field;
|
||||
$page->untrackChange($field);
|
||||
}
|
||||
}
|
||||
|
||||
if($quiet) {
|
||||
// do not save native properties or update page modified-user/modified
|
||||
|
||||
} else {
|
||||
// finish by saving the page without fields
|
||||
$options['quiet'] = false;
|
||||
|
||||
foreach($page->getChanges() as $name) {
|
||||
if($page->hasField($name)) continue;
|
||||
// add only changed native properties to saved list
|
||||
$saved[$name] = $name;
|
||||
}
|
||||
|
||||
$options['noFields'] = true;
|
||||
|
||||
if($noHooks) {
|
||||
$this->save($page, $options);
|
||||
} else {
|
||||
$this->pages->save($page, $options);
|
||||
}
|
||||
}
|
||||
|
||||
$this->pages->debugLog('saveFields', "$page:" . implode(',', $fields), $saved);
|
||||
|
||||
return $saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Silently add status flag to a Page and save
|
||||
*
|
||||
@@ -959,7 +1073,7 @@ class PagesEditor extends Wire {
|
||||
*
|
||||
*/
|
||||
public function saveStatus(Page $page) {
|
||||
return $this->savePageStatus($page, $page->status) > 0;
|
||||
return $this->savePageStatus($page, $page->status, false, 2) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -985,29 +1099,34 @@ class PagesEditor extends Wire {
|
||||
$database = $this->wire()->database;
|
||||
$rowCount = 0;
|
||||
$multi = is_array($pageID) || $pageID instanceof PageArray;
|
||||
$page = $pageID instanceof Page ? $pageID : null;
|
||||
$status = (int) $status;
|
||||
|
||||
if($status < 0 || $status > Page::statusMax) {
|
||||
throw new WireException("status must be between 0 and " . Page::statusMax);
|
||||
}
|
||||
|
||||
$sql = "UPDATE pages SET status=";
|
||||
$sqlUpdate = "UPDATE pages SET status=";
|
||||
|
||||
if($remove === 2) {
|
||||
// overwrite status (internal/undocumented)
|
||||
$sql .= "status=$status";
|
||||
$sqlUpdate .= "status=$status";
|
||||
if($page instanceof Page) $page->status = $status;
|
||||
} else if($remove) {
|
||||
// remove status
|
||||
$sql .= "status & ~$status";
|
||||
$sqlUpdate .= "status & ~$status";
|
||||
if($page instanceof Page) $page->removeStatus($status);
|
||||
} else {
|
||||
// add status
|
||||
$sql .= "status|$status";
|
||||
$sqlUpdate .= "status|$status";
|
||||
if($page instanceof Page) $page->addStatus($status);
|
||||
}
|
||||
|
||||
if($multi && $recursive) {
|
||||
// multiple page IDs combined with recursive option, must be handled individually
|
||||
foreach($pageID as $id) {
|
||||
$rowCount += $this->savePageStatus((int) "$id", $status, $recursive, $remove);
|
||||
$id = $id instanceof Page ? $id : (int) "$id";
|
||||
$rowCount += $this->savePageStatus($id, $status, $recursive, $remove);
|
||||
}
|
||||
// exit early in this case
|
||||
return $rowCount;
|
||||
@@ -1019,15 +1138,17 @@ class PagesEditor extends Wire {
|
||||
$id = (int) "$id";
|
||||
if($id > 0) $ids[$id] = $id;
|
||||
}
|
||||
if(!count($ids)) $ids[] = 0;
|
||||
$query = $database->prepare("$sql WHERE id IN(" . implode(',', $ids) . ")");
|
||||
$database->execute($query);
|
||||
return $query->rowCount();
|
||||
if(count($ids)) {
|
||||
$query = $database->prepare("$sqlUpdate WHERE id IN(" . implode(',', $ids) . ")");
|
||||
$database->execute($query);
|
||||
$rowCount = $query->rowCount();
|
||||
}
|
||||
return $rowCount;
|
||||
|
||||
} else {
|
||||
// single page ID or Page object
|
||||
$pageID = (int) "$pageID";
|
||||
$query = $database->prepare("$sql WHERE id=:page_id");
|
||||
$query = $database->prepare("$sqlUpdate WHERE id=:page_id");
|
||||
$query->bindValue(":page_id", $pageID, \PDO::PARAM_INT);
|
||||
$database->execute($query);
|
||||
$rowCount = $query->rowCount();
|
||||
@@ -1037,12 +1158,13 @@ class PagesEditor extends Wire {
|
||||
|
||||
// recursive mode assumed from this point forward
|
||||
$parentIDs = array($pageID);
|
||||
$ids = [];
|
||||
|
||||
do {
|
||||
$parentID = array_shift($parentIDs);
|
||||
|
||||
// update all children to have the same status
|
||||
$query = $database->prepare("$sql WHERE parent_id=:parent_id");
|
||||
$query = $database->prepare("$sqlUpdate WHERE parent_id=:parent_id");
|
||||
$query->bindValue(":parent_id", $parentID, \PDO::PARAM_INT);
|
||||
$database->execute($query);
|
||||
$rowCount += $query->rowCount();
|
||||
@@ -1062,18 +1184,24 @@ class PagesEditor extends Wire {
|
||||
|
||||
/** @noinspection PhpAssignmentInConditionInspection */
|
||||
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
|
||||
$parentIDs[] = (int) $row['id'];
|
||||
$id = (int) $row['id'];
|
||||
$parentIDs[$id] = $id;
|
||||
$ids[$id] = $id;
|
||||
}
|
||||
|
||||
$query->closeCursor();
|
||||
|
||||
} while(count($parentIDs));
|
||||
|
||||
if(count($ids)) {
|
||||
$rowCount += $this->savePageStatus($ids, $status, false, $remove);
|
||||
}
|
||||
|
||||
return $rowCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently delete a page and it's fields.
|
||||
* Permanently delete a page and its fields.
|
||||
*
|
||||
* Unlike trash(), pages deleted here are not restorable.
|
||||
*
|
||||
@@ -1096,19 +1224,35 @@ class PagesEditor extends Wire {
|
||||
'uncacheAll' => false,
|
||||
'recursive' => is_bool($recursive) ? $recursive : false,
|
||||
// internal use properties:
|
||||
'_level' => 0,
|
||||
// internal recursion level: incremented only by delete operations initiated by this method
|
||||
'_level' => 0,
|
||||
// internal delete branch: Page object when deleting a branch
|
||||
'_deleteBranch' => false,
|
||||
);
|
||||
|
||||
// page IDs for all delete operations, cleared out once no longer recursive
|
||||
static $deleted = array();
|
||||
|
||||
// external recursion level: all recursive delete operations including those initiated from hooks
|
||||
static $level = 0;
|
||||
|
||||
if(is_array($recursive)) $options = $recursive;
|
||||
$options = array_merge($defaults, $options);
|
||||
|
||||
// check if page already deleted in a recursive call
|
||||
if(isset($deleted[$page->id])) {
|
||||
// page already deleted, return result from that call
|
||||
return $options['recursive'] ? $deleted[$page->id] : true;
|
||||
}
|
||||
|
||||
$this->isDeleteable($page, true); // throws WireException
|
||||
|
||||
$numDeleted = 0;
|
||||
$numChildren = $page->numChildren;
|
||||
$deleteBranch = false;
|
||||
$level++;
|
||||
|
||||
if($numChildren) {
|
||||
if($numChildren) try {
|
||||
if(!$options['recursive']) {
|
||||
throw new WireException("Can't delete Page $page because it has one or more children.");
|
||||
}
|
||||
@@ -1119,17 +1263,21 @@ class PagesEditor extends Wire {
|
||||
}
|
||||
foreach($page->children('include=all') as $child) {
|
||||
/** @var Page $child */
|
||||
if(isset($deleted[$child->id])) continue;
|
||||
$options['_level']++;
|
||||
$result = $this->pages->delete($child, true, $options);
|
||||
$options['_level']--;
|
||||
if(!$result) throw new WireException("Error doing recursive page delete, stopped by page $child");
|
||||
$numDeleted += $result;
|
||||
}
|
||||
} catch(\Exception $e) {
|
||||
$level = 0;
|
||||
$deleted = array();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// trigger a hook to indicate delete is ready and WILL occur
|
||||
$this->pages->deleteReady($page, $options);
|
||||
|
||||
$this->clear($page);
|
||||
|
||||
$database = $this->wire()->database;
|
||||
@@ -1140,10 +1288,20 @@ class PagesEditor extends Wire {
|
||||
$this->pages->sortfields()->delete($page);
|
||||
$page->setTrackChanges(false);
|
||||
$page->status = Page::statusDeleted; // no need for bitwise addition here, as this page is no longer relevant
|
||||
$this->pages->deleted($page, $options);
|
||||
$numDeleted++;
|
||||
$deleted[$page->id] = $numDeleted;
|
||||
$this->pages->deleted($page, $options);
|
||||
|
||||
if($deleteBranch) $this->pages->deletedBranch($page, $options, $numDeleted);
|
||||
if($options['uncacheAll']) $this->pages->uncacheAll($page);
|
||||
|
||||
if($level > 0) $level--;
|
||||
if($level < 1) {
|
||||
// back at root call, reset all tracking
|
||||
$deleted = array();
|
||||
$level = 0;
|
||||
}
|
||||
|
||||
$this->pages->debugLog('delete', $page, true);
|
||||
|
||||
return $options['recursive'] ? $numDeleted : true;
|
||||
@@ -1153,7 +1311,7 @@ class PagesEditor extends Wire {
|
||||
* Clone an entire page (including fields, file assets, and optionally children) and return it.
|
||||
*
|
||||
* @param Page $page Page that you want to clone
|
||||
* @param Page $parent New parent, if different (default=same parent)
|
||||
* @param Page|null $parent New parent, if different (default=same parent)
|
||||
* @param bool $recursive Clone the children too? (default=true)
|
||||
* @param array|string $options Optional options that can be passed to clone or save
|
||||
* - forceID (int): force a specific ID
|
||||
@@ -1163,7 +1321,7 @@ class PagesEditor extends Wire {
|
||||
* @throws WireException|\Exception on fatal error
|
||||
*
|
||||
*/
|
||||
public function _clone(Page $page, Page $parent = null, $recursive = true, $options = array()) {
|
||||
public function _clone(Page $page, ?Page $parent = null, $recursive = true, $options = array()) {
|
||||
|
||||
$defaults = array(
|
||||
'forceID' => 0,
|
||||
|
@@ -18,7 +18,7 @@
|
||||
*
|
||||
* Note: all the "change" prefix options require update=true.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2017 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*/
|
||||
@@ -35,9 +35,9 @@ class PagesExportImport extends Wire {
|
||||
*/
|
||||
public function getExportPath($subdir = '') {
|
||||
|
||||
/** @var WireFileTools $files */
|
||||
$files = $this->wire('files');
|
||||
$path = $this->wire('config')->paths->assets . 'backups/' . $this->className() . '/';
|
||||
$files = $this->wire()->files;
|
||||
$config = $this->wire()->config;
|
||||
$path = $config->paths->assets . 'backups/' . $this->className() . '/';
|
||||
|
||||
$readmeText = "When this file is present, files and directories in here are auto-deleted after a short period of time.";
|
||||
$readmeFile = $this->className() . '.txt';
|
||||
@@ -75,8 +75,7 @@ class PagesExportImport extends Wire {
|
||||
*/
|
||||
public function cleanupFiles($maxAge = 3600) {
|
||||
|
||||
/** @var WireFileTools $files */
|
||||
$files = $this->wire('files');
|
||||
$files = $this->wire()->files;
|
||||
$path = $this->getExportPath();
|
||||
$qty = 0;
|
||||
|
||||
@@ -111,13 +110,12 @@ class PagesExportImport extends Wire {
|
||||
*
|
||||
* @param PageArray $items
|
||||
* @param array $options
|
||||
* @return string|bool Path+filename to ZIP file or boolean false on failure
|
||||
* @return string Path+filename to ZIP file
|
||||
*
|
||||
*/
|
||||
public function exportZIP(PageArray $items, array $options = array()) {
|
||||
|
||||
/** @var WireFileTools $files */
|
||||
$files = $this->wire('files');
|
||||
$files = $this->wire()->files;
|
||||
|
||||
$options['exportTarget'] = 'zip';
|
||||
$zipPath = $this->getExportPath();
|
||||
@@ -173,7 +171,7 @@ class PagesExportImport extends Wire {
|
||||
$path = $tempDir->get();
|
||||
$options['filesPath'] = $path;
|
||||
|
||||
$zipFileItems = $this->wire('files')->unzip($filename, $path);
|
||||
$zipFileItems = $this->wire()->files->unzip($filename, $path);
|
||||
|
||||
if(empty($zipFileItems)) return false;
|
||||
|
||||
@@ -192,7 +190,7 @@ class PagesExportImport extends Wire {
|
||||
*
|
||||
* @param PageArray $items
|
||||
* @param array $options
|
||||
* @return string|bool JSON string of pages or boolean false on error
|
||||
* @return string JSON string of pages
|
||||
*
|
||||
*/
|
||||
public function exportJSON(PageArray $items, array $options = array()) {
|
||||
@@ -232,8 +230,9 @@ class PagesExportImport extends Wire {
|
||||
*/
|
||||
public function pagesToArray(PageArray $items, array $options = array()) {
|
||||
|
||||
/** @var Config $config */
|
||||
$config = $this->wire('config');
|
||||
$config = $this->wire()->config;
|
||||
$modules = $this->wire()->modules;
|
||||
$fields = $this->wire()->fields;
|
||||
|
||||
$defaults = array(
|
||||
'verbose' => false,
|
||||
@@ -247,7 +246,7 @@ class PagesExportImport extends Wire {
|
||||
'type' => 'ProcessWire:PageArray',
|
||||
'created' => date('Y-m-d H:i:s'),
|
||||
'version' => $config->version,
|
||||
'user' => $this->wire('user')->name,
|
||||
'user' => $this->wire()->user->name,
|
||||
'host' => $config->httpHost,
|
||||
'pages' => array(),
|
||||
'fields' => array(),
|
||||
@@ -260,7 +259,7 @@ class PagesExportImport extends Wire {
|
||||
);
|
||||
|
||||
if($items->getLimit()) {
|
||||
$pageNum = $this->wire('input')->pageNum;
|
||||
$pageNum = $this->wire()->input->pageNum;
|
||||
$a['pagination'] = array(
|
||||
'start' => $items->getStart(),
|
||||
'limit' => $items->getLimit(),
|
||||
@@ -273,8 +272,7 @@ class PagesExportImport extends Wire {
|
||||
unset($a['pagination']);
|
||||
}
|
||||
|
||||
/** @var Languages $languages */
|
||||
$languages = $this->wire('languages');
|
||||
$languages = $this->wire()->languages;
|
||||
if($languages) $languages->setDefault();
|
||||
$templates = array();
|
||||
|
||||
@@ -293,9 +291,9 @@ class PagesExportImport extends Wire {
|
||||
}
|
||||
foreach($fieldNames as $fieldName) {
|
||||
if(isset($a['fields'][$fieldName])) continue;
|
||||
$field = $this->wire('fields')->get($fieldName);
|
||||
$field = $fields->get($fieldName);
|
||||
if(!$field || !$field->type) continue;
|
||||
$moduleInfo = $this->wire('modules')->getModuleInfoVerbose($field->type);
|
||||
$moduleInfo = $modules->getModuleInfoVerbose($field->type);
|
||||
if($options['verbose']) {
|
||||
$fieldData = $field->getExportData();
|
||||
unset($fieldData['name']);
|
||||
@@ -363,8 +361,8 @@ class PagesExportImport extends Wire {
|
||||
$of = $page->of();
|
||||
$page->of(false);
|
||||
|
||||
/** @var Languages $languages */
|
||||
$languages = $this->wire('languages');
|
||||
/** @var Languages|Language[] $languages */
|
||||
$languages = $this->wire()->languages;
|
||||
if($languages) $languages->setDefault();
|
||||
$numFiles = 0;
|
||||
|
||||
@@ -457,7 +455,7 @@ class PagesExportImport extends Wire {
|
||||
*
|
||||
* @param array $a
|
||||
* @param array $options
|
||||
* @return PageArray|bool
|
||||
* @return PageArray|int
|
||||
* @throws WireException
|
||||
*
|
||||
*/
|
||||
@@ -476,7 +474,7 @@ class PagesExportImport extends Wire {
|
||||
if(!empty($options['pageArray']) && $options['pageArray'] instanceof PageArray) {
|
||||
$pageArray = $options['pageArray'];
|
||||
} else {
|
||||
$pageArray = $this->wire('pages')->newPageArray();
|
||||
$pageArray = $this->wire()->pages->newPageArray();
|
||||
}
|
||||
$count = 0;
|
||||
|
||||
@@ -494,7 +492,7 @@ class PagesExportImport extends Wire {
|
||||
foreach($a['pages'] as $item) {
|
||||
$page = $this->arrayToPage($item, $options);
|
||||
$id = $item['settings']['id'];
|
||||
$this->wire('notices')->move($page, $pageArray, array('prefix' => "Page $id: "));
|
||||
$this->wire()->notices->move($page, $pageArray, array('prefix' => "Page $id: "));
|
||||
if(!$options['count']) $pageArray->add($page);
|
||||
$count++;
|
||||
}
|
||||
@@ -680,8 +678,7 @@ class PagesExportImport extends Wire {
|
||||
*/
|
||||
protected function importGetPage(array &$a, array &$options, array &$errors) {
|
||||
|
||||
/** @var Pages $pages */
|
||||
$pages = $this->wire('pages');
|
||||
$pages = $this->wire()->pages;
|
||||
$path = $a['path'];
|
||||
|
||||
/** @var Page|NullPage $page */
|
||||
@@ -738,7 +735,7 @@ class PagesExportImport extends Wire {
|
||||
if(is_object($template)) {
|
||||
// ok
|
||||
} else {
|
||||
$template = $this->wire('templates')->get($template);
|
||||
$template = $this->wire()->templates->get($template);
|
||||
}
|
||||
if($template) {
|
||||
$options['template'] = $template;
|
||||
@@ -762,12 +759,12 @@ class PagesExportImport extends Wire {
|
||||
// determine parent
|
||||
static $previousPaths = array();
|
||||
$usePrevious = true;
|
||||
$pages = $this->wire('pages');
|
||||
$pages = $this->wire()->pages;
|
||||
$path = $a['path'];
|
||||
|
||||
if($options['parent']) {
|
||||
// parent specified in options
|
||||
if(is_object($options['parent']) && $options['parent'] instanceof Page) {
|
||||
if($options['parent'] instanceof Page) {
|
||||
$parent = $options['parent'];
|
||||
} else if(ctype_digit("$options[parent]")) {
|
||||
$parent = $pages->get((int) $options['parent']);
|
||||
@@ -841,7 +838,7 @@ class PagesExportImport extends Wire {
|
||||
if(!$isNew) $options['changeTemplate'] = false;
|
||||
$template = $options['template'];
|
||||
$parent = $options['parent'];
|
||||
$languages = $this->wire('languages');
|
||||
$languages = $this->wire()->languages;
|
||||
$langProperties = array();
|
||||
|
||||
// populate page base settings
|
||||
@@ -980,9 +977,9 @@ class PagesExportImport extends Wire {
|
||||
$page->trackChange("{$field->name}__");
|
||||
}
|
||||
}
|
||||
if(is_object($pageValue) && $pageValue instanceof Wire) {
|
||||
if($pageValue instanceof Wire) {
|
||||
// movie notices from the pageValue to the page
|
||||
$this->wire('notices')->move($pageValue, $page);
|
||||
$this->wire()->notices->move($pageValue, $page);
|
||||
}
|
||||
} else {
|
||||
// test import on existing page, avoids actually setting value to the page
|
||||
@@ -1020,9 +1017,8 @@ class PagesExportImport extends Wire {
|
||||
// 'file3.gif' => [ ... see above ... ],
|
||||
// ];
|
||||
|
||||
/** @var Pagefiles $pagefiles */
|
||||
$pagefiles = $page->get($field->name);
|
||||
if(!$pagefiles || !$pagefiles instanceof Pagefiles) {
|
||||
if(!$pagefiles instanceof Pagefiles) {
|
||||
$page->warning("Unable to import files to field '$field->name' because it is not a files field");
|
||||
return;
|
||||
}
|
||||
@@ -1033,7 +1029,7 @@ class PagesExportImport extends Wire {
|
||||
$variationsAdded = array();
|
||||
|
||||
$maxFiles = (int) $field->get('maxFiles');
|
||||
$languages = $this->wire('languages');
|
||||
$languages = $this->wire()->languages;
|
||||
$filesPath = $pagefiles->path();
|
||||
/** @var null|WireHttp $http */
|
||||
$http = null;
|
||||
@@ -1137,7 +1133,7 @@ class PagesExportImport extends Wire {
|
||||
|
||||
if($sourceExists) {
|
||||
// copy variation from options[filesPath]
|
||||
if($this->wire('files')->copy($sourceFile, $targetFile)) {
|
||||
if($this->wire()->files->copy($sourceFile, $targetFile)) {
|
||||
$variationsAdded[] = $name;
|
||||
} else {
|
||||
$page->warning("Unable to copy file (image variation): $sourceFile");
|
||||
@@ -1243,13 +1239,9 @@ class PagesExportImport extends Wire {
|
||||
$numExisting = 0;
|
||||
$numNew = 0;
|
||||
|
||||
/** @var Pages $pages */
|
||||
$pages = $this->wire('pages');
|
||||
/** @var Fields $fields */
|
||||
$fields = $this->wire('fields');
|
||||
/** @var Sanitizer $sanitizer */
|
||||
$sanitizer = $this->wire('sanitizer');
|
||||
/** @var PageFinder $pageFinder */
|
||||
$pages = $this->wire()->pages;
|
||||
$fields = $this->wire()->fields;
|
||||
$sanitizer = $this->wire()->sanitizer;
|
||||
$pageFinder = $this->wire(new PageFinder());
|
||||
|
||||
// Identify missing fields
|
||||
@@ -1300,7 +1292,7 @@ class PagesExportImport extends Wire {
|
||||
|
||||
// determine which templates are missing, and which fields are missing from templates
|
||||
foreach($templateNames as $templateName => $fieldNames) {
|
||||
$template = $this->wire('templates')->get($templateName);
|
||||
$template = $this->wire()->templates->get($templateName);
|
||||
if($template) {
|
||||
// template exists
|
||||
$missingTemplateFields[$templateName] = array();
|
||||
@@ -1316,7 +1308,7 @@ class PagesExportImport extends Wire {
|
||||
}
|
||||
|
||||
// determine which parents are missing
|
||||
foreach($parentPaths as $key => $path) {
|
||||
foreach($parentPaths as /* $key => */ $path) {
|
||||
if(isset($pagePaths[$path])) {
|
||||
// this parent already exists or will be created during import
|
||||
} else {
|
||||
|
@@ -698,7 +698,8 @@ class PagesLoader extends Wire {
|
||||
foreach($row as $key => $value) {
|
||||
if(strpos($key, '__')) {
|
||||
if($value === null) {
|
||||
$row[$key] = 'null'; // ensure detected by later isset in foreach($joinFields)
|
||||
// $row[$key] = 'null'; // ensure detected by later isset in foreach($joinFields)
|
||||
$row[$key] = new NullField();
|
||||
} else {
|
||||
$page->setFieldValue($key, $value, false);
|
||||
}
|
||||
@@ -712,7 +713,10 @@ class PagesLoader extends Wire {
|
||||
if(!$template->fieldgroup->hasField($joinField)) continue;
|
||||
$field = $page->getField($joinField);
|
||||
if(!$field || !$field->type) continue;
|
||||
if(isset($row["{$joinField}__data"])) {
|
||||
$v = isset($row["{$joinField}__data"]) ? $row["{$joinField}__data"] : null;
|
||||
if($v instanceof NullField) $v = null;
|
||||
// if(isset($row["{$joinField}__data"])) {
|
||||
if($v !== null) {
|
||||
if(!$field->hasFlag(Field::flagAutojoin)) {
|
||||
$field->addFlag(Field::flagAutojoin);
|
||||
$tmpAutojoinFields[$field->id] = $field;
|
||||
@@ -785,25 +789,31 @@ class PagesLoader extends Wire {
|
||||
$options = array_merge($defaults, $options);
|
||||
$items = $this->pages->find($selector, $options);
|
||||
$page = $items->first();
|
||||
|
||||
if($page && !$page->viewable(false)) {
|
||||
|
||||
if(isset($options['findAll']) && $options['findAll'] === true) {
|
||||
// page is always allowed through when findAll=true
|
||||
} else if(isset($options['include']) && $options['include'] === 'all') {
|
||||
// page is always allowed through when include=all
|
||||
} else if($page && !$page->viewable(false)) {
|
||||
// page found but is not viewable, check if include mode was specified and would allow the page
|
||||
$include = isset($options['include']) ? strtolower($options['include']) : null;
|
||||
$checkAccess = true;
|
||||
$selectors = $items->getSelectors();
|
||||
if($selectors) {
|
||||
$include = $selectors->getSelectorByField('include');
|
||||
if($include === null) {
|
||||
$include = $selectors->getSelectorByField('include');
|
||||
if($include) $include = strtolower($include->value());
|
||||
}
|
||||
$checkAccess = $selectors->getSelectorByField('check_access');
|
||||
if(!$checkAccess) $checkAccess = $selectors->getSelectorByField('checkAccess');
|
||||
$checkAccess = $checkAccess ? (bool) $checkAccess->value() : true;
|
||||
} else {
|
||||
$include = null;
|
||||
$checkAccess = true;
|
||||
}
|
||||
if(!$include) {
|
||||
// there was no “include=” selector present
|
||||
if($checkAccess === true) $page = null;
|
||||
} else if($include->value() === 'all') {
|
||||
} else if($include === 'all') {
|
||||
// allow $page to pass through with include=all mode
|
||||
} else if($include->value() === 'unpublished' && $page->hasStatus(Page::statusUnpublished) && $checkAccess) {
|
||||
} else if($include === 'unpublished' && $page->isUnpublished() && $checkAccess) {
|
||||
// check if user would have access without unpublished status
|
||||
$status = $page->status;
|
||||
$page->setQuietly('status', $status & ~Page::statusUnpublished);
|
||||
@@ -2022,6 +2032,332 @@ class PagesLoader extends Wire {
|
||||
return $this->pages->find($selector, $options)->getTotal();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Preload/Prefetch fields for page together as a group (experimental)
|
||||
*
|
||||
* This is an optimization that enables you to load the values for multiple fields into
|
||||
* a page at once, and often in a single query. This is similar to the `joinFields` option
|
||||
* when loading a page, or the `autojoin` option configured with a field, except that it
|
||||
* can be used after a page is already loaded. It provides a performance improvement
|
||||
* relative lazy-loading of fields individually as they are accessed.
|
||||
*
|
||||
* Preload works only with Fieldtypes that do not override the core’s loading methods.
|
||||
* Preload also does not work with FieldtypeMulti types at present, except for the Page
|
||||
* Fieldtype when configured to load a single page. Though it can be enabled for testing
|
||||
* purposes using the `useFieldtypeMulti` $options argument.
|
||||
*
|
||||
* NOTE: This function is currently experimental, recommended for testing only.
|
||||
*
|
||||
* @param Page $page Page to preload fields for
|
||||
* @param array $fieldNames Names of fields to preload
|
||||
* @param array $options
|
||||
* - `debug` (bool): Specify true to include additional debug info in return value (default=false).
|
||||
* - `useFieldtypeMulti` (bool): Enable FieldtypeMulti for testing purposes (default=false).
|
||||
* - `loadPageRefs` (bool): Optimization to early load pages in page reference fields? (default=true)
|
||||
* @return array Array containing what was loaded and skipped
|
||||
* @since 3.0.243
|
||||
*
|
||||
*/
|
||||
public function preloadFields(Page $page, array $fieldNames, $options = array()) {
|
||||
|
||||
$defaults = [
|
||||
'debug' => is_bool($options) ? $options : false,
|
||||
'useFieldtypeMulti' => false,
|
||||
'loadPageRefs' => true,
|
||||
];
|
||||
|
||||
static $level = 0;
|
||||
|
||||
$options = is_array($options) ? array_merge($defaults, $options) : $defaults;
|
||||
$debug = $options['debug'];
|
||||
$database = $this->wire()->database;
|
||||
$fieldNames = array_unique($fieldNames);
|
||||
$fields = $page->wire()->fields;
|
||||
$loadFields = [];
|
||||
$loadedFields = [];
|
||||
$selects = [];
|
||||
$joins = [];
|
||||
$numJoins = 0;
|
||||
$maxJoins = 60;
|
||||
|
||||
$log = [
|
||||
'loaded' => [],
|
||||
'skipped' => [],
|
||||
'blank' => [],
|
||||
'queries' => 1,
|
||||
];
|
||||
|
||||
if(!$page->id || !$page->template) return $log;
|
||||
|
||||
foreach($fieldNames as $fieldKey => $fieldName) {
|
||||
|
||||
// identify which fields to load and which to skip
|
||||
$field = $fields->get($fieldName);
|
||||
$fieldName = $field ? $field->name : '';
|
||||
$fieldNames[$fieldKey] = $fieldName;
|
||||
$error = $field ? $this->skipPreloadField($page, $field, $options) : 'Field not found';
|
||||
|
||||
if($error) {
|
||||
unset($fieldNames[$fieldKey]);
|
||||
if($fieldName) $log['skipped'][] = "$fieldName ($error)";
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldtype = $field->type;
|
||||
$schema = $fieldtype->trimDatabaseSchema($fieldtype->getDatabaseSchema($field));
|
||||
$numJoins += count($schema);
|
||||
|
||||
if($numJoins >= $maxJoins) break;
|
||||
|
||||
$loadFields[$fieldName] = $field;
|
||||
$table = $field->getTable();
|
||||
|
||||
// build selects and joins
|
||||
foreach(array_keys($schema) as $colName) {
|
||||
if($options['useFieldtypeMulti'] && $fieldtype instanceof FieldtypeMulti) {
|
||||
$sep = FieldtypeMulti::multiValueSeparator;
|
||||
$orderBy = "ORDER BY $table.sort";
|
||||
$selects[] = "GROUP_CONCAT($table.$colName $orderBy SEPARATOR '$sep') AS `{$table}__$colName`";
|
||||
} else {
|
||||
$selects[] = "$table.$colName AS {$table}__$colName";
|
||||
}
|
||||
$joins[$table] = "LEFT JOIN $table ON $table.pages_id=pages.id";
|
||||
}
|
||||
|
||||
unset($fieldNames[$fieldKey]);
|
||||
}
|
||||
|
||||
if(!count($selects)) return $log;
|
||||
|
||||
$trackChanges = $level ? null : $page->trackChanges();
|
||||
if($trackChanges) $page->setTrackChanges(false);
|
||||
|
||||
$level++;
|
||||
$timer = $debug ? Debug::timer() : false;
|
||||
|
||||
// build and execute the query
|
||||
$sql =
|
||||
'SELECT ' . implode(",\n", $selects) . ' ' .
|
||||
"\nFROM pages " .
|
||||
"\n" . implode(" \n", $joins) . ' ' .
|
||||
"\nWHERE pages.id=:pid";
|
||||
|
||||
$query = $database->prepare($sql);
|
||||
$query->bindValue(':pid', $page->id, \PDO::PARAM_INT);
|
||||
$query->execute();
|
||||
|
||||
$data = [];
|
||||
$row = $query->fetch(\PDO::FETCH_ASSOC);
|
||||
$query->closeCursor();
|
||||
|
||||
// combine data from DB into column groups by field name
|
||||
if($row) {
|
||||
foreach($row as $key => $value) {
|
||||
list($table, $colName) = explode('__', $key, 2);
|
||||
list(, $fieldName) = explode('_', $table, 2);
|
||||
if(!isset($data[$fieldName])) $data[$fieldName] = [];
|
||||
$data[$fieldName][$colName] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// wake up loaded values and populate to $page
|
||||
$pageIds = [];
|
||||
|
||||
foreach($data as $fieldName => $sleepValue) {
|
||||
if(!isset($loadFields[$fieldName])) {
|
||||
unset($data[$fieldName]);
|
||||
continue;
|
||||
}
|
||||
$field = $loadFields[$fieldName];
|
||||
$fieldtype = $field->type;
|
||||
$cols = array_keys($sleepValue);
|
||||
if(count($cols) === 1 && array_key_exists('data', $sleepValue)) {
|
||||
$sleepValue = $sleepValue['data'];
|
||||
}
|
||||
if($sleepValue === null) {
|
||||
unset($data[$fieldName]);
|
||||
continue; // force to getBlankValue in loop below this
|
||||
}
|
||||
if($options['useFieldtypeMulti'] && $fieldtype instanceof FieldtypeMulti) {
|
||||
if(strrpos($sleepValue, FieldtypeMulti::multiValueSeparator)) {
|
||||
$sleepValue = explode(FieldtypeMulti::multiValueSeparator, $sleepValue);
|
||||
}
|
||||
}
|
||||
if($fieldtype instanceof FieldtypePage && $sleepValue && $options['loadPageRefs']) {
|
||||
if(!is_array($sleepValue)) $sleepValue = [ $sleepValue ];
|
||||
foreach($sleepValue as $pageId) {
|
||||
$pageId = (int) $pageId;
|
||||
if(!$pageId) continue;
|
||||
if($this->pages->cacher()->hasCache($pageId)) continue;
|
||||
$parentId = $field->get('parent_id');
|
||||
$templateId = FieldtypePage::getTemplateIDs($field, true);
|
||||
if(!ctype_digit("$parentId")) $parentId = 0;
|
||||
if(!ctype_digit("$templateId")) $templateId = 0;
|
||||
$groupKey = "$parentId,$templateId";
|
||||
if(!isset($pageIds[$groupKey])) $pageIds[$groupKey] = [];
|
||||
$pageIds[$groupKey][$pageId] = $pageId;
|
||||
}
|
||||
}
|
||||
|
||||
$data[$fieldName] = $sleepValue;
|
||||
}
|
||||
|
||||
// preload all pages in template or parent groups
|
||||
if(count($pageIds)) {
|
||||
foreach($pageIds as $groupKey => $ids) {
|
||||
list($parentId, $templateId) = explode(',', $groupKey);
|
||||
$this->pages->getByID($ids, [ 'template' => $templateId, 'parent_id' => $parentId ]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach($data as $fieldName => $sleepValue) {
|
||||
$field = $loadFields[$fieldName];
|
||||
$fieldtype = $field->type;
|
||||
$value = $fieldtype->wakeupValue($page, $field, $sleepValue);
|
||||
$page->_parentSet($field->name, $value);
|
||||
$loadedFields[$field->name] = $fieldName;
|
||||
unset($loadFields[$field->name]);
|
||||
$log['loaded'][] = $fieldName;
|
||||
}
|
||||
|
||||
// any remaining loadFields not present in DB should get blank value
|
||||
foreach($loadFields as $field) {
|
||||
$value = $field->type->getBlankValue($page, $field);
|
||||
$fieldName = $field->name;
|
||||
$page->_parentSet($fieldName, $value);
|
||||
$log['blank'][] = $fieldName;
|
||||
}
|
||||
|
||||
// go recursive for any remaining fields
|
||||
if(count($fieldNames)) {
|
||||
$result = $this->preloadFields($page, $fieldNames, $options);
|
||||
foreach($log as $key => $value) {
|
||||
if(is_array($value)) {
|
||||
$log[$key] = array_merge($value, $result[$key]);
|
||||
} else if(is_int($value)) {
|
||||
$log[$key] += $result[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$level--;
|
||||
|
||||
if($debug && $timer && !$level) $log['timer'] = Debug::timer($timer);
|
||||
|
||||
if($trackChanges) $page->setTrackChanges($trackChanges);
|
||||
|
||||
return $log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload all supported fields for given page (experimental)
|
||||
*
|
||||
* NOTE: This function is currently experimental, recommended for testing only.
|
||||
*
|
||||
* @param Page $page Page to preload fields for
|
||||
* @param array $options
|
||||
* - `debug` (bool): Specify true to return array of debug info (default=false).
|
||||
* - `skipFieldNames` (array): Optional names of fields to skip over (default=[]).
|
||||
* - See the `PagesLoader::preloadFields()` method for additional options.
|
||||
* @return array Array of details
|
||||
* @since 3.0.243
|
||||
*
|
||||
*/
|
||||
public function preloadAllFields(Page $page, $options = array()) {
|
||||
$fieldNames = [];
|
||||
$skipFieldNames = isset($options['skipFieldNames']) ? $options['skipFieldNames'] : false;
|
||||
foreach($page->template->fieldgroup as $field) {
|
||||
if($skipFieldNames && in_array($field->name, $skipFieldNames)) continue;
|
||||
$fieldNames[] = $field->name;
|
||||
}
|
||||
return $this->preloadFields($page, $fieldNames, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip preloading of this field or fieldtype?
|
||||
*
|
||||
* Returns populated string with reason if yes, or blank string if no.
|
||||
*
|
||||
* @param Page $page
|
||||
* @param Field $field
|
||||
* @param array $options
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
protected function skipPreloadField(Page $page, Field $field, array $options) {
|
||||
|
||||
static $fieldtypeErrors = [];
|
||||
|
||||
$useFieldtypeMulti = isset($options['useFieldtypeMulti']) ? $options['useFieldtypeMulti'] : false;
|
||||
$error = '';
|
||||
|
||||
if($page->_parentGet($field->name) !== null) {
|
||||
$error = 'Already loaded';
|
||||
} else if(!$page->template->fieldgroup->hasField($field)) {
|
||||
$error = "Template '$page->template' does not have field";
|
||||
} else if(!$field->getTable()) {
|
||||
$error = 'Field has no table';
|
||||
}
|
||||
|
||||
if($error) return $error;
|
||||
|
||||
$fieldtype = $field->type;
|
||||
$shortName = $fieldtype->shortName;
|
||||
$cacheName = $shortName;
|
||||
|
||||
if($fieldtype instanceof FieldtypePage) {
|
||||
$cacheName .= $field->get('derefAsPage');
|
||||
}
|
||||
|
||||
if(isset($fieldtypeErrors[$cacheName])) {
|
||||
return $fieldtypeErrors[$cacheName];
|
||||
}
|
||||
|
||||
// fieldtype status not yet known
|
||||
$schema = $fieldtype->getDatabaseSchema($field);
|
||||
$xtra = isset($schema['xtra']) ? $schema['xtra'] : [];
|
||||
|
||||
if($fieldtype instanceof FieldtypeMulti) {
|
||||
if($useFieldtypeMulti) {
|
||||
// allow group_concat for FieldtypeMulti
|
||||
} else if($fieldtype instanceof FieldtypePage && $field->get('derefAsPage') > 0) {
|
||||
// allow single-page matches
|
||||
} else {
|
||||
$error = "$shortName: Unsupported without useFieldtypeMulti=true";
|
||||
}
|
||||
} else if($fieldtype instanceof FieldtypeFieldsetOpen) {
|
||||
$error = 'Fieldset: Unsupported';
|
||||
}
|
||||
|
||||
if(!$error && isset($xtra['all']) && $xtra['all'] === false) {
|
||||
if($shortName !== 'Repeater' && $shortName !== 'RepeaterMatrix') {
|
||||
$error = "$shortName: External storage";
|
||||
}
|
||||
}
|
||||
|
||||
if(!$error) {
|
||||
$ref = new \ReflectionClass($fieldtype);
|
||||
// identify parent class that implements loadPageField method
|
||||
$info = $ref->getMethod('___loadPageField');
|
||||
$class = wireClassName($info->class);
|
||||
// whitelist of classes with custom loadPageField methods we support
|
||||
$rootClasses = [
|
||||
'Fieldtype',
|
||||
'FieldtypeMulti',
|
||||
'FieldtypeTextarea',
|
||||
'FieldtypeTextareaLanguage'
|
||||
];
|
||||
if(!in_array($class, $rootClasses)) {
|
||||
$error = "$shortName: Has custom loader";
|
||||
}
|
||||
}
|
||||
|
||||
$fieldtypeErrors[$cacheName] = $error;
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove pages from already-loaded PageArray aren't visible or accessible
|
||||
*
|
||||
|
@@ -91,6 +91,18 @@ class PagesLoaderCache extends Wire {
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is given page ID in the cache?
|
||||
*
|
||||
* @param int page ID
|
||||
* @return bool
|
||||
* @since 3.0.243
|
||||
*
|
||||
*/
|
||||
public function hasCache($id) {
|
||||
return isset($this->pageIdCache[$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache the given page.
|
||||
*
|
||||
@@ -149,13 +161,13 @@ class PagesLoaderCache extends Wire {
|
||||
/**
|
||||
* Remove all pages from the cache
|
||||
*
|
||||
* @param Page $page Optional Page that initiated the uncacheAll
|
||||
* @param Page|null $page Optional Page that initiated the uncacheAll
|
||||
* @param array $options Additional options to modify behavior:
|
||||
* - `shallow` (bool): By default, this method also calls $page->uncache(). To prevent call to $page->uncache(), set 'shallow' => true.
|
||||
* @return int Number of pages uncached
|
||||
*
|
||||
*/
|
||||
public function uncacheAll(Page $page = null, array $options = array()) {
|
||||
public function uncacheAll(?Page $page = null, array $options = array()) {
|
||||
|
||||
if($page) {} // to ignore unused parameter inspection
|
||||
$user = $this->wire()->user;
|
||||
|
@@ -578,7 +578,12 @@ class PagesParents extends Wire {
|
||||
*/
|
||||
public function movePage(Page $page, Page $oldParent, Page $newParent) {
|
||||
|
||||
$key = "$page,$oldParent,$newParent";
|
||||
if($key === $this->movePageLast) return 0;
|
||||
$this->movePageLast = $key;
|
||||
|
||||
$database = $this->wire()->database;
|
||||
$numChildren = $page->numChildren();
|
||||
$numRows = 0;
|
||||
|
||||
$oldParentIds = $oldParent->parents()->explode('id');
|
||||
@@ -595,7 +600,11 @@ class PagesParents extends Wire {
|
||||
$query->bindValue(':new_parent_id', $newParent->id, \PDO::PARAM_INT);
|
||||
$query->bindValue(':pages_id', $page->id, \PDO::PARAM_INT);
|
||||
$query->bindValue(':old_parent_id', $oldParent->id, \PDO::PARAM_INT);
|
||||
$query->execute();
|
||||
try {
|
||||
$query->execute();
|
||||
} catch(\Exception $e) {
|
||||
if($e->getCode() != 23000) throw $e;
|
||||
}
|
||||
$numRows += $query->rowCount();
|
||||
|
||||
// find children and descendents of the page that moved
|
||||
@@ -604,7 +613,7 @@ class PagesParents extends Wire {
|
||||
$query->bindValue(':pages_id', $page->id, \PDO::PARAM_INT);
|
||||
$query->execute();
|
||||
|
||||
$ids = array();
|
||||
$ids = array($page->id => $page->id);
|
||||
while($row = $query->fetch(\PDO::FETCH_NUM)) {
|
||||
$id = (int) $row[0];
|
||||
$ids[$id] = $id;
|
||||
@@ -612,17 +621,30 @@ class PagesParents extends Wire {
|
||||
|
||||
$query->closeCursor();
|
||||
|
||||
if(!count($ids)) return $numRows;
|
||||
|
||||
$inserts = array();
|
||||
|
||||
foreach($ids as $id) {
|
||||
foreach($newParentIds as $parentId) {
|
||||
$inserts["$id,$parentId"] = array('pages_id' => $id, 'parents_id' => (int) $parentId);
|
||||
if($id === $parentId) continue;
|
||||
$inserts[] = "$id,$parentId";
|
||||
}
|
||||
}
|
||||
|
||||
// redundancy to capture specific missing parent situations
|
||||
foreach($newParent->parents() as $parent) {
|
||||
if($parent->id < 2) continue;
|
||||
$inserts[] = "$newParent->id,$parent->id";
|
||||
if($parent->parent_id > 1) {
|
||||
$grandParent = $parent->parent();
|
||||
$inserts[] = "$parent->id,$grandParent->id";
|
||||
}
|
||||
}
|
||||
|
||||
if(count($oldParentIds)) {
|
||||
// if page has children also add it to the inserts list
|
||||
if($numChildren) $inserts[] = "$page->id,$newParent->id";
|
||||
|
||||
// delete old parent IDs
|
||||
if(count($oldParentIds) && count($ids)) {
|
||||
$idStr = implode(',', $ids);
|
||||
$oldParentIds = $this->wire()->sanitizer->intArray($oldParentIds);
|
||||
$oldParentIdStr = implode(',', $oldParentIds);
|
||||
@@ -636,18 +658,25 @@ class PagesParents extends Wire {
|
||||
$query = $database->prepare($sql);
|
||||
|
||||
foreach($inserts as $insert) {
|
||||
$query->bindValue(':pages_id', $insert['pages_id'], \PDO::PARAM_INT);
|
||||
$query->bindValue(':parents_id', $insert['parents_id'], \PDO::PARAM_INT);
|
||||
list($id, $parentId) = explode(',', $insert, 2);
|
||||
$query->bindValue(':pages_id', $id, \PDO::PARAM_INT);
|
||||
$query->bindValue(':parents_id', $parentId, \PDO::PARAM_INT);
|
||||
try {
|
||||
if($query->execute()) $numRows++;
|
||||
} catch(\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
if($e->getCode() != 23000) $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $numRows;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
*/
|
||||
protected $movePageLast = '';
|
||||
|
||||
/**
|
||||
* Add rows for a new parent in the pages_parents table
|
||||
*
|
||||
|
@@ -15,7 +15,7 @@
|
||||
* afterwards when appropriate.
|
||||
* #pw-body
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @todo:
|
||||
@@ -131,9 +131,9 @@ class PagesPathFinder extends Wire {
|
||||
protected function init($path, array $options) {
|
||||
|
||||
$this->options = array_merge($this->defaults, $options);
|
||||
$this->verbose = $this->options['verbose'];
|
||||
$this->methods = array();
|
||||
$this->useLanguages = $this->options['useLanguages'] ? $this->languages(true) : array();
|
||||
$this->verbose = $this->options['verbose'] || !empty($this->useLanguages);
|
||||
$this->result = $this->getBlankResult(array('request' => $path));
|
||||
$this->template = null;
|
||||
$this->admin = null;
|
||||
@@ -380,8 +380,10 @@ class PagesPathFinder extends Wire {
|
||||
*
|
||||
*/
|
||||
protected function applyPagesRow(array $parts, $row) {
|
||||
|
||||
$maxUrlSegmentLength = $this->wire()->config->maxUrlSegmentLength;
|
||||
|
||||
$config = $this->wire()->config;
|
||||
$maxUrlSegmentLength = $config->maxUrlSegmentLength;
|
||||
$maxUrlSegments = $config->maxUrlSegments;
|
||||
$result = &$this->result;
|
||||
|
||||
// array of [language name] => [ 'a', 'b', 'c' ] (from /a/b/c/)
|
||||
@@ -396,14 +398,28 @@ class PagesPathFinder extends Wire {
|
||||
|
||||
if(!$id) {
|
||||
// if it didn’t resolve to DB page name then it is a URL segment
|
||||
if(strlen($name) > $maxUrlSegmentLength) $name = substr($name, 0, $maxUrlSegmentLength);
|
||||
$result['urlSegments'][] = $name;
|
||||
if($this->verbose) {
|
||||
$result['parts'][] = array(
|
||||
'type' => 'urlSegment',
|
||||
'value' => $name,
|
||||
'language' => ''
|
||||
);
|
||||
if(strlen($name) > $maxUrlSegmentLength) {
|
||||
$name = substr($name, 0, $maxUrlSegmentLength);
|
||||
if($config->longUrlResponse >= 300) {
|
||||
$result['response'] = $config->longUrlResponse;
|
||||
$this->addResultError('urlSegmentLength', 'URL segment length > config.maxUrlSegmentLength');
|
||||
}
|
||||
}
|
||||
if(count($result['urlSegments']) + 1 > $maxUrlSegments) {
|
||||
if($config->longUrlResponse >= 300) {
|
||||
$this->addResultError('urlSegmentMAX', 'Number of URL segments exceeds config.maxUrlSegments');
|
||||
$result['response'] = $config->longUrlResponse;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$result['urlSegments'][] = $name;
|
||||
if($this->verbose) {
|
||||
$result['parts'][] = array(
|
||||
'type' => 'urlSegment',
|
||||
'value' => $name,
|
||||
'language' => ''
|
||||
);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -480,7 +496,7 @@ class PagesPathFinder extends Wire {
|
||||
* If language segment detected then remove it and populate language to result
|
||||
*
|
||||
* @param string $path
|
||||
* @return array|bool
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
protected function getPathParts($path) {
|
||||
@@ -497,7 +513,7 @@ class PagesPathFinder extends Wire {
|
||||
$lastPart = '';
|
||||
|
||||
if($this->strlen($path) > $maxPathLength) {
|
||||
$result['response'] = 414; // 414=URI too long
|
||||
$result['response'] = $config->longUrlResponse; // 414=URI too long
|
||||
$this->addResultError('pathLengthMAX', "Path length exceeds max allowed $maxPathLength");
|
||||
$path = substr($path, 0, $maxPathLength);
|
||||
}
|
||||
@@ -506,7 +522,7 @@ class PagesPathFinder extends Wire {
|
||||
|
||||
if(count($parts) > $maxDepth) {
|
||||
$parts = array_slice($parts, 0, $maxDepth);
|
||||
$result['response'] = 414;
|
||||
$result['response'] = $config->longUrlResponse;
|
||||
$this->addResultError('pathDepthMAX', 'Path depth exceeds config.maxUrlDepth');
|
||||
} else if($path === '/' || $path === '' || !count($parts)) {
|
||||
return array();
|
||||
@@ -789,7 +805,7 @@ class PagesPathFinder extends Wire {
|
||||
|
||||
// if there were any non-default language segments, let that dictate the language
|
||||
if(empty($result['language']['segment'])) {
|
||||
$useLangName = 'default';
|
||||
$useLangName = count($result['parts']) ? 'default' : $result['language']['name'];
|
||||
foreach($result['parts'] as $part) {
|
||||
$langName = $part['language'];
|
||||
if(empty($langName) || $langName === 'default') continue;
|
||||
@@ -934,7 +950,7 @@ class PagesPathFinder extends Wire {
|
||||
|
||||
$result['methods'] = $this->methods;
|
||||
|
||||
if(!$this->verbose) unset($result['parts'], $result['methods']);
|
||||
if(!$this->options['verbose']) unset($result['parts'], $result['methods']);
|
||||
|
||||
if(empty($errors)) {
|
||||
// force errors placeholder to end if there aren’t any
|
||||
@@ -1484,7 +1500,7 @@ class PagesPathFinder extends Wire {
|
||||
*
|
||||
*/
|
||||
protected function addResultError($name, $message, $force = false) {
|
||||
if(!$this->verbose && !$force) return;
|
||||
//if(!$this->verbose && !$force) return;
|
||||
$this->result['errors'][$name] = $message;
|
||||
}
|
||||
|
||||
|
@@ -837,7 +837,7 @@ class PagesRawFinder extends Wire {
|
||||
if(!isset($templatesById[$templateId])) $templatesById[$templateId] = $templates->get($templateId);
|
||||
$template = $templatesById[$templateId]; /** @var Template $template */
|
||||
$slash = $template->slashUrls ? '/' : '';
|
||||
$path = strlen($value) && $value !== '/' ? "$value$slash" : '';
|
||||
$path = strlen("$value") && $value !== '/' ? "$value$slash" : '';
|
||||
if(isset($this->runtimeFields['url'])) {
|
||||
$this->values[$id]['url'] = $rootUrl . $path;
|
||||
}
|
||||
@@ -861,11 +861,11 @@ class PagesRawFinder extends Wire {
|
||||
protected function findCustom() {
|
||||
if(count($this->customFields)) {
|
||||
// one or more custom fields requested
|
||||
if($this->ids === null) {
|
||||
if($this->ids === null && !empty($this->selector)) {
|
||||
// only find IDs if we didn’t already in the nativeFields section
|
||||
$this->setIds($this->findIDs($this->selector, false));
|
||||
}
|
||||
if(!count($this->ids)) return;
|
||||
if(empty($this->ids)) return;
|
||||
foreach($this->customFields as $fieldName => $field) {
|
||||
/** @var Field $field */
|
||||
$cols = isset($this->customCols[$fieldName]) ? $this->customCols[$fieldName] : array();
|
||||
@@ -1144,11 +1144,12 @@ class PagesRawFinder extends Wire {
|
||||
$this->wire($finder);
|
||||
$options = $this->options;
|
||||
$options['indexed'] = true;
|
||||
$pageRefRows = $finder->find($pageRefIds, $pageRefCols, $options);
|
||||
$pageRefRows = count($pageRefIds) ? $finder->find($pageRefIds, $pageRefCols, $options) : array();
|
||||
|
||||
foreach($this->values as $pageId => $pageRow) {
|
||||
if(!isset($pageRow[$fieldName])) continue;
|
||||
foreach($pageRow[$fieldName] as $pageRefId) {
|
||||
if(!isset($pageRefRows[$pageRefId])) continue;
|
||||
$this->values[$pageId][$fieldName][$pageRefId] = $pageRefRows[$pageRefId];
|
||||
}
|
||||
if(!$this->getMultiple && $field->get('derefAsPage') > 0) {
|
||||
|
@@ -403,6 +403,25 @@ class PagesRequest extends Wire {
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of page info (as provided by PagePathFinder)
|
||||
*
|
||||
* See the PagesPathFinder::get() method return value for a description of
|
||||
* what this method returns.
|
||||
*
|
||||
* If this method returns a blank array, it means that the getPage()
|
||||
* method has not yet been called or that it did not match a page.
|
||||
*
|
||||
* #pw-advanced
|
||||
*
|
||||
* @return array
|
||||
* @since 3.0.242
|
||||
*
|
||||
*/
|
||||
public function getPageInfo() {
|
||||
return $this->pageInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update/get page for given user
|
||||
@@ -574,9 +593,15 @@ class PagesRequest extends Wire {
|
||||
}
|
||||
|
||||
$maxUrlDepth = $config->maxUrlDepth;
|
||||
if($maxUrlDepth > 0 && substr_count($it, '/') > $config->maxUrlDepth) {
|
||||
$this->setResponseCode(414, 'Request URL exceeds max depth set in $config->maxUrlDepth');
|
||||
return false;
|
||||
if($maxUrlDepth > 0 && substr_count($it, '/') > $maxUrlDepth) {
|
||||
if(in_array($config->longUrlResponse, [ 302, 301 ])) {
|
||||
$parts = array_slice(explode('/', $it), 0, $maxUrlDepth);
|
||||
$it = '/' . trim(implode('/', $parts), '/') . '/';
|
||||
$this->setRedirectPath($it, $config->longUrlResponse);
|
||||
} else {
|
||||
$this->setResponseCode($config->longUrlResponse, 'Request URL exceeds max depth set in $config->maxUrlDepth');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if(!isset($it[0]) || $it[0] != '/') $it = "/$it";
|
||||
@@ -710,7 +735,7 @@ class PagesRequest extends Wire {
|
||||
* @return string|Page|null Login page object or string w/redirect URL, null if 404
|
||||
*
|
||||
*/
|
||||
public function ___getLoginPageOrUrl(Page $page = null) {
|
||||
public function ___getLoginPageOrUrl(?Page $page = null) {
|
||||
|
||||
$config = $this->wire()->config;
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
*
|
||||
* Implements page trash/restore/empty methods of the $pages API variable
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*/
|
||||
@@ -18,6 +18,14 @@ class PagesTrash extends Wire {
|
||||
*/
|
||||
protected $pages;
|
||||
|
||||
/**
|
||||
* Last action, i.e. "restore:1234"
|
||||
*
|
||||
* @var int
|
||||
*
|
||||
*/
|
||||
protected $lastAction = '';
|
||||
|
||||
/**
|
||||
* Construct
|
||||
*
|
||||
@@ -41,17 +49,18 @@ class PagesTrash extends Wire {
|
||||
*
|
||||
*/
|
||||
public function trash(Page $page, $save = true) {
|
||||
|
||||
|
||||
if(!$this->pages->isDeleteable($page) || $page->template->noTrash) {
|
||||
throw new WireException("This page (id=$page->id) may not be placed in the trash");
|
||||
}
|
||||
|
||||
$trash = $this->pages->get($this->config->trashPageID);
|
||||
$trash = $this->pages->get($this->wire()->config->trashPageID);
|
||||
|
||||
if(!$trash->id) {
|
||||
throw new WireException("Unable to load trash page defined by config::trashPageID");
|
||||
}
|
||||
|
||||
$this->pages->trashReady($page);
|
||||
if($this->lastAction != "trash:$page") $this->pages->trashReady($page);
|
||||
|
||||
$page->addStatus(Page::statusTrash);
|
||||
|
||||
@@ -70,10 +79,10 @@ class PagesTrash extends Wire {
|
||||
// make the name unique when in trash, to avoid namespace collision and maintain parent restore info
|
||||
$name = $page->id;
|
||||
if($parentPrevious && $parentPrevious->id) {
|
||||
$name .= "." . $parentPrevious->id;
|
||||
$name .= "." . $page->sort;
|
||||
$sort = $page->get('sortPrevious|sort');
|
||||
$name .= ".$parentPrevious->id.$sort";
|
||||
}
|
||||
$page->name = ($name . "_" . $page->name);
|
||||
$page->name = ($name . '_' . $page->name);
|
||||
|
||||
// do the same for other languages, if present
|
||||
$languages = $this->wire()->languages;
|
||||
@@ -87,9 +96,13 @@ class PagesTrash extends Wire {
|
||||
}
|
||||
}
|
||||
|
||||
if($save) $this->pages->save($page);
|
||||
$this->lastAction = "trash:$page";
|
||||
|
||||
if($save) {
|
||||
$this->pages->save($page, array('caller' => 'pages.trash', 'callback' => 'trashed'));
|
||||
}
|
||||
|
||||
$this->pages->editor()->savePageStatus($page->id, Page::statusTrash, true, false);
|
||||
if($save) $this->pages->trashed($page);
|
||||
$this->pages->debugLog('trash', $page, true);
|
||||
|
||||
return true;
|
||||
@@ -107,24 +120,31 @@ class PagesTrash extends Wire {
|
||||
*
|
||||
*/
|
||||
public function restore(Page $page, $save = true) {
|
||||
|
||||
$info = $this->getRestoreInfo($page, true);
|
||||
|
||||
if($info['restorable']) {
|
||||
// we detected original parent
|
||||
if($save) $page->save();
|
||||
if($this->lastAction !== "restore:$page") $this->pages->restoreReady($page);
|
||||
$this->pages->editor()->savePageStatus($page->id, Page::statusTrash, true, true);
|
||||
|
||||
} else if(!$page->parent->isTrash()) {
|
||||
// page has had new parent already set
|
||||
if($this->lastAction !== "restore:$page") $this->pages->restoreReady($page);
|
||||
$page->removeStatus(Page::statusTrash);
|
||||
if($save) $page->save();
|
||||
$this->pages->editor()->savePageStatus($page->id, Page::statusTrash, true, true);
|
||||
if($save) $this->pages->restored($page);
|
||||
$this->pages->debugLog('restore', $page, true);
|
||||
} else {
|
||||
// page is in trash and we cannot detect new parent
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->lastAction = "restore:$page";
|
||||
|
||||
if($save) {
|
||||
$this->pages->save($page, array('caller' => 'pages.restore', 'callback' => 'restored'));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -221,6 +241,7 @@ class PagesTrash extends Wire {
|
||||
|
||||
if($populateToPage) {
|
||||
$page->name = $name;
|
||||
$page->removeStatus(Page::statusTrash);
|
||||
if($newParent) {
|
||||
$page->sort = $sort;
|
||||
$page->parent = $newParent;
|
||||
|
@@ -17,6 +17,7 @@
|
||||
* https://processwire.com
|
||||
*
|
||||
* @method Page add($name)
|
||||
* @method Page new(array $options = []) 3.0.249
|
||||
* @method bool save(Page $page)
|
||||
* @method bool delete(Page $page, $recursive = false)
|
||||
*
|
||||
@@ -89,6 +90,23 @@ class PagesType extends Wire implements \IteratorAggregate, \Countable {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new instance of this page type
|
||||
*
|
||||
* @param array $options
|
||||
* @return Page
|
||||
* @since 3.0.249
|
||||
*
|
||||
*/
|
||||
public function ___new(array $options = []) {
|
||||
$defaults = array(
|
||||
'template' => $this->getTemplate(),
|
||||
'parent' => $this->getParent(),
|
||||
'pageClass' => $this->getPageClass()
|
||||
);
|
||||
return $this->wire()->pages->newPage(array_merge($defaults, $options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one or more templates that this PagesType represents
|
||||
*
|
||||
|
@@ -54,7 +54,7 @@
|
||||
*
|
||||
* #pw-body
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* This file is licensed under the MIT license
|
||||
@@ -108,6 +108,14 @@ class Paths extends WireData {
|
||||
*/
|
||||
protected $_root = '';
|
||||
|
||||
/**
|
||||
* As used by get() method
|
||||
*
|
||||
* @var null
|
||||
*
|
||||
*/
|
||||
protected $_http = null;
|
||||
|
||||
/**
|
||||
* Construct the Paths
|
||||
*
|
||||
@@ -117,6 +125,7 @@ class Paths extends WireData {
|
||||
public function __construct($root) {
|
||||
$this->_root = $root;
|
||||
$this->useFuel(false);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,20 +174,19 @@ class Paths extends WireData {
|
||||
*
|
||||
*/
|
||||
public function get($key) {
|
||||
static $_http = null;
|
||||
if($key === 'root') return $this->_root;
|
||||
$http = '';
|
||||
$altKey = '';
|
||||
if(is_object($key)) {
|
||||
$key = "$key";
|
||||
} else if(strpos($key, 'http') === 0) {
|
||||
if(is_null($_http)) {
|
||||
if($this->_http === null) {
|
||||
$scheme = $this->wire()->input->scheme;
|
||||
if(!$scheme) $scheme = 'http';
|
||||
$httpHost = $this->wire()->config->httpHost;
|
||||
if($httpHost) $_http = "$scheme://$httpHost";
|
||||
if($httpHost) $this->_http = "$scheme://$httpHost";
|
||||
}
|
||||
$http = $_http;
|
||||
$http = $this->_http;
|
||||
$key = substr($key, 4); // httpTemplates => Templates
|
||||
$altKey = $key; // no lowercase conversion (useful for keys like module names, i.e. 'ProcessPageEdit')
|
||||
$key[0] = strtolower($key[0]); // first character lowercase: Templates => templates
|
||||
|
@@ -47,10 +47,10 @@ class Permission extends Page {
|
||||
/**
|
||||
* Create a new Permission page in memory.
|
||||
*
|
||||
* @param Template $tpl Template object this page should use.
|
||||
* @param Template|null $tpl Template object this page should use.
|
||||
*
|
||||
*/
|
||||
public function __construct(Template $tpl = null) {
|
||||
public function __construct(?Template $tpl = null) {
|
||||
parent::__construct($tpl);
|
||||
if(!$tpl) $this->template = $this->wire()->templates->get('permission');
|
||||
$this->_parent_id = $this->wire()->config->permissionsPageID;
|
||||
@@ -145,5 +145,3 @@ class Permission extends Page {
|
||||
return $this->wire()->permissions;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@@ -16,6 +16,7 @@
|
||||
* @method void added(Page $page) Hook called just after a Permission is added #pw-hooker
|
||||
* @method void deleteReady(Page $page) Hook called before a Permission is deleted #pw-hooker
|
||||
* @method void deleted(Page $page) Hook called after a permission is deleted #pw-hooker
|
||||
* @method Permission new($options = []) Create new Permission instance in memory (3.0.249+)
|
||||
*
|
||||
*/
|
||||
class Permissions extends PagesType {
|
||||
|
@@ -251,6 +251,8 @@ abstract class Process extends WireData implements Module {
|
||||
*
|
||||
*/
|
||||
public function ___breadcrumb($href, $label) {
|
||||
if(is_array($label)) return $this;
|
||||
$label = (string) $label;
|
||||
$pos = strpos($label, '/');
|
||||
if($pos !== false && strpos($href, '/') === false) {
|
||||
// arguments got reversed, we'll work with it anyway...
|
||||
|
@@ -328,7 +328,8 @@ class ProcessController extends Wire {
|
||||
public function ___execute() {
|
||||
|
||||
$debug = $this->wire()->config->debug;
|
||||
$breadcrumbs = $this->wire()->breadcrumbs;
|
||||
$breadcrumbs = $this->wire()->breadcrumbs;
|
||||
$adminTheme = $this->wire()->adminTheme;
|
||||
$headline = $this->wire('processHeadline');
|
||||
$numBreadcrumbs = $breadcrumbs ? count($breadcrumbs) : null;
|
||||
$process = $this->getProcess();
|
||||
@@ -398,6 +399,20 @@ class ProcessController extends Wire {
|
||||
$content = '';
|
||||
}
|
||||
}
|
||||
|
||||
if(!$process instanceof WirePageEditor) {
|
||||
$headline = (string) $this->wire('processHeadline');
|
||||
if(strlen($headline)) {
|
||||
if(strpos($headline, '<icon-') === false) {
|
||||
// $icon = $this->wire()->modules->getModuleInfoProperty('icon');
|
||||
// if($icon) $process->headline("<icon-$icon> $headline");
|
||||
} else {
|
||||
if(!$adminTheme instanceof AdminThemeFramework) {
|
||||
$process->headline(preg_replace('/(?:<|<)icon-[-a-z0-9]+(?:>|>)/', '', $headline));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
@@ -486,18 +501,22 @@ class ProcessController extends Wire {
|
||||
/**
|
||||
* Generate a message in JSON format, for use with AJAX output
|
||||
*
|
||||
* @param string $msg
|
||||
* @param bool $error
|
||||
* @param bool $allowMarkup
|
||||
* @param string|array $msg Message string or in 3.0.246+ also accepts an array of extra data
|
||||
* When using an array, please include a 'message' index with text about the error or non-error.
|
||||
* @param bool $error Is this in error message? Default is true, or specify false if not.
|
||||
* @param bool $allowMarkup Allow markup in message? Applies only to $msg string or 'message' index of array (default=false)
|
||||
* @return string JSON encoded string
|
||||
*
|
||||
*/
|
||||
public function jsonMessage($msg, $error = false, $allowMarkup = false) {
|
||||
if(!$allowMarkup) $msg = $this->wire()->sanitizer->entities($msg);
|
||||
return json_encode(array(
|
||||
'error' => (bool) $error,
|
||||
'message' => (string) $msg
|
||||
));
|
||||
$a = array('error' => (bool) $error, 'message' => '');
|
||||
if(is_array($msg)) {
|
||||
$a = array_merge($a, $msg);
|
||||
} else {
|
||||
$a['message'] = (string) $msg;
|
||||
}
|
||||
if(!$allowMarkup) $a['message'] = $this->wire()->sanitizer->entities($a['message']);
|
||||
return json_encode($a);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -17,7 +17,7 @@ require_once(__DIR__ . '/boot.php');
|
||||
* ~~~~~
|
||||
* #pw-body
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* Default API vars (A-Z)
|
||||
@@ -54,6 +54,7 @@ require_once(__DIR__ . '/boot.php');
|
||||
* @property Users $users
|
||||
* @property ProcessWire $wire
|
||||
* @property WireShutdown $shutdown
|
||||
* @property PagesVersions|null $pagesVersions
|
||||
*
|
||||
* @method init()
|
||||
* @method ready()
|
||||
@@ -79,7 +80,7 @@ class ProcessWire extends Wire {
|
||||
* Reversion revision number
|
||||
*
|
||||
*/
|
||||
const versionRevision = 227;
|
||||
const versionRevision = 251;
|
||||
|
||||
/**
|
||||
* Version suffix string (when applicable)
|
||||
@@ -287,7 +288,7 @@ class ProcessWire extends Wire {
|
||||
|
||||
// this is reset in the $this->setConfig() method based on current debug mode
|
||||
ini_set('display_errors', true);
|
||||
error_reporting(E_ALL | E_STRICT);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
$config->setWire($this);
|
||||
|
||||
@@ -448,7 +449,7 @@ class ProcessWire extends Wire {
|
||||
|
||||
if($debug) {
|
||||
// If debug mode is on then echo all errors
|
||||
error_reporting(E_ALL | E_STRICT);
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
} else {
|
||||
// disable all error reporting
|
||||
@@ -600,7 +601,9 @@ class ProcessWire extends Wire {
|
||||
// the current user can only be determined after the session has been initiated
|
||||
$session = $this->wire('session', new Session($this), true);
|
||||
$this->initVar('session', $session);
|
||||
$this->wire('user', $users->getCurrentUser());
|
||||
$user = $users->getCurrentUser();
|
||||
if($config->userOutputFormatting) $user->of(true);
|
||||
$this->wire('user', $user);
|
||||
|
||||
$input = $this->wire('input', new WireInput(), true);
|
||||
if($config->wireInputLazy) $input->setLazy(true);
|
||||
|
@@ -71,6 +71,14 @@ class Punycode {
|
||||
*/
|
||||
protected $encoding;
|
||||
|
||||
/**
|
||||
* PHP mb string functions supported?
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
*/
|
||||
protected $mb = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@@ -78,7 +86,12 @@ class Punycode {
|
||||
*/
|
||||
public function __construct($encoding = 'UTF-8') {
|
||||
$this->encoding = $encoding;
|
||||
$this->mb = function_exists("mb_internal_encoding");
|
||||
}
|
||||
|
||||
public function strtolower($str) { return $this->mb ? mb_strtolower($str, $this->encoding) : strtolower($str); }
|
||||
public function strlen($str) { return $this->mb ? mb_strlen($str, $this->encoding) : strlen($str); }
|
||||
public function substr($str, $a, $b) { return $this->mb ? mb_substr($str, $a, $b, $this->encoding) : substr($str, $a, $b); }
|
||||
|
||||
/**
|
||||
* Encode a domain to its Punycode version
|
||||
@@ -88,7 +101,7 @@ class Punycode {
|
||||
* @return string Punycode representation in ASCII
|
||||
*/
|
||||
public function encode($input) {
|
||||
$input = mb_strtolower($input, $this->encoding);
|
||||
$input = $this->strtolower($input);
|
||||
$parts = explode('.', $input);
|
||||
foreach($parts as &$part) {
|
||||
$part = $this->encodePart($part);
|
||||
@@ -122,7 +135,7 @@ class Punycode {
|
||||
$codePoints['nonBasic'] = array_unique($codePoints['nonBasic']);
|
||||
sort($codePoints['nonBasic']);
|
||||
$i = 0;
|
||||
$length = mb_strlen($input, $this->encoding);
|
||||
$length = $this->strlen($input);
|
||||
while($h < $length) {
|
||||
$m = $codePoints['nonBasic'][$i++];
|
||||
$delta = $delta + ($m - $n) * ($h + 1);
|
||||
@@ -138,11 +151,11 @@ class Punycode {
|
||||
if($q < $t) {
|
||||
break;
|
||||
}
|
||||
$code = $t + (($q - $t) % (static::BASE - $t));
|
||||
$code = $t + ((floor($q) - $t) % (static::BASE - $t));
|
||||
$output .= static::$encodeTable[$code];
|
||||
$q = ($q - $t) / (static::BASE - $t);
|
||||
}
|
||||
$output .= static::$encodeTable[$q];
|
||||
$output .= static::$encodeTable[floor($q)];
|
||||
$bias = $this->adapt($delta, $h + 1, ($h === $b));
|
||||
$delta = 0;
|
||||
$h++;
|
||||
@@ -209,9 +222,9 @@ class Punycode {
|
||||
$bias = $this->adapt($i - $oldi, ++$outputLength, ($oldi === 0));
|
||||
$n = $n + (int) ($i / $outputLength);
|
||||
$i = $i % ($outputLength);
|
||||
$output = mb_substr($output, 0, $i, $this->encoding) .
|
||||
$output = $this->substr($output, 0, $i) .
|
||||
$this->codePointToChar($n) .
|
||||
mb_substr($output, $i, $outputLength - 1, $this->encoding);
|
||||
$this->substr($output, $i, $outputLength - 1);
|
||||
$i++;
|
||||
}
|
||||
return $output;
|
||||
@@ -272,9 +285,9 @@ class Punycode {
|
||||
'basic' => array(),
|
||||
'nonBasic' => array(),
|
||||
);
|
||||
$length = mb_strlen($input, $this->encoding);
|
||||
$length = $this->strlen($input);
|
||||
for($i = 0; $i < $length; $i++) {
|
||||
$char = mb_substr($input, $i, 1, $this->encoding);
|
||||
$char = $this->substr($input, $i, 1);
|
||||
$code = $this->charToCodePoint($char);
|
||||
if($code < 128) {
|
||||
$codePoints['all'][] = $codePoints['basic'][] = $code;
|
||||
@@ -323,4 +336,4 @@ class Punycode {
|
||||
return chr(($code >> 18) + 240) . chr((($code >> 12) & 63) + 128) . chr((($code >> 6) & 63) + 128) . chr(($code & 63) + 128);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -25,10 +25,10 @@ class Role extends Page {
|
||||
/**
|
||||
* Create a new Role page in memory.
|
||||
*
|
||||
* @param Template $tpl
|
||||
* @param Template|null $tpl
|
||||
*
|
||||
*/
|
||||
public function __construct(Template $tpl = null) {
|
||||
public function __construct(?Template $tpl = null) {
|
||||
parent::__construct($tpl);
|
||||
}
|
||||
|
||||
|
@@ -14,6 +14,7 @@
|
||||
* @method void added(Page $page) Hook called just after a Role is added #pw-hooker
|
||||
* @method void deleteReady(Page $page) Hook called before a Role is deleted #pw-hooker
|
||||
* @method void deleted(Page $page) Hook called after a Role is deleted #pw-hooker
|
||||
* @method Role new($options = []) Create new Role instance in memory (3.0.249+)
|
||||
*
|
||||
*/
|
||||
|
||||
|
@@ -90,7 +90,7 @@
|
||||
*
|
||||
* #pw-body
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @link https://processwire.com/api/variables/sanitizer/ Offical $sanitizer API variable Documentation
|
||||
@@ -238,6 +238,17 @@ class Sanitizer extends Wire {
|
||||
'‍', // zero width join
|
||||
);
|
||||
|
||||
/**
|
||||
* Characters blacklisted from UTF-8 page names
|
||||
*
|
||||
* @var string[]
|
||||
*
|
||||
*/
|
||||
protected $pageNameBlacklist = array(
|
||||
'/', '\\', '%', '"', "'", '<', '>', '?', '!', '#', '@', ':', ';', ',',
|
||||
'+', '=', '*', '^', '$', '(', ')', '[', ']', '{', '}', '|', '&',
|
||||
);
|
||||
|
||||
/**
|
||||
* Sanitizer method names (A-Z) and type(s) they return
|
||||
*
|
||||
@@ -366,8 +377,6 @@ class Sanitizer extends Wire {
|
||||
*/
|
||||
public function nameFilter($value, array $allowedExtras, $replacementChar, $beautify = false, $maxLength = 128) {
|
||||
|
||||
static $replacements = array();
|
||||
|
||||
if(!is_string($value)) $value = $this->string($value);
|
||||
$allowed = array_merge($this->allowedASCII, $allowedExtras);
|
||||
$needsWork = strlen(str_replace($allowed, '', $value));
|
||||
@@ -376,27 +385,30 @@ class Sanitizer extends Wire {
|
||||
if($beautify && $needsWork) {
|
||||
if($beautify === self::translate && $this->multibyteSupport) {
|
||||
$value = mb_strtolower($value);
|
||||
$replacements = array();
|
||||
|
||||
if(empty($replacements)) {
|
||||
if(empty($this->caches['nameFilterReplace'])) {
|
||||
$modules = $this->wire()->modules;
|
||||
if($modules) {
|
||||
$configData = $this->wire()->modules->getModuleConfigData('InputfieldPageName');
|
||||
$replacements = empty($configData['replacements']) ? InputfieldPageName::$defaultReplacements : $configData['replacements'];
|
||||
$replacements = $this->wire()->modules->getConfig('InputfieldPageName', 'replacements');
|
||||
if(empty($replacements)) $replacements = InputfieldPageName::$defaultReplacements;
|
||||
$this->caches['nameFilterReplace'] = $replacements;
|
||||
}
|
||||
} else {
|
||||
$replacements = $this->caches['nameFilterReplace'];
|
||||
}
|
||||
|
||||
foreach($replacements as $from => $to) {
|
||||
if(mb_strpos($value, $from) !== false) {
|
||||
$value = mb_eregi_replace($from, $to, $value);
|
||||
}
|
||||
|
||||
if(count($replacements)) {
|
||||
$value = str_replace(array_keys($replacements), array_values($replacements), $value);
|
||||
$needsWork = strlen(str_replace($allowed, '', $value));
|
||||
}
|
||||
}
|
||||
|
||||
if(function_exists("\\iconv")) {
|
||||
if($needsWork && function_exists("\\iconv")) {
|
||||
$v = iconv("UTF-8", "ASCII//TRANSLIT//IGNORE", $value);
|
||||
if($v) $value = $v;
|
||||
$needsWork = strlen(str_replace($allowed, '', $value));
|
||||
}
|
||||
$needsWork = strlen(str_replace($allowed, '', $value));
|
||||
}
|
||||
|
||||
if(strlen($value) > $maxLength) $value = substr($value, 0, $maxLength);
|
||||
@@ -776,7 +788,8 @@ class Sanitizer extends Wire {
|
||||
* - `Sanitizer::okUTF8` (constant): Allow UTF-8 characters to appear in path (implied if $config->pageNameCharset is 'UTF8').
|
||||
* @param int|array $maxLength Maximum number of characters allowed in the name.
|
||||
* You may also specify the $options array for this argument instead.
|
||||
* @param array $options Array of options to modify default behavior. See Sanitizer::name() method for available options.
|
||||
* @param array $options Array of options to modify default behavior. See Sanitizer::name() method for available options, plus:
|
||||
* - `punycodeVersion` (int): Punycode version to use with UTF-8 page names, see Sanitizer::getPunycodeVersion() method for details.
|
||||
* @return string
|
||||
* @see Sanitizer::name()
|
||||
*
|
||||
@@ -787,7 +800,8 @@ class Sanitizer extends Wire {
|
||||
if(!strlen($value)) return '';
|
||||
|
||||
$defaults = array(
|
||||
'charset' => $this->wire()->config->pageNameCharset
|
||||
'charset' => $this->wire()->config->pageNameCharset,
|
||||
'punycodeVersion' => 0,
|
||||
);
|
||||
|
||||
if(is_array($beautify)) {
|
||||
@@ -819,19 +833,26 @@ class Sanitizer extends Wire {
|
||||
&& !ctype_alnum(str_replace(array('-', '_', '.'), '', $value))
|
||||
&& strpos($value, 'xn-') !== 0) {
|
||||
|
||||
$tt = $this->getTextTools();
|
||||
$max = $maxLength;
|
||||
|
||||
do {
|
||||
// encode value
|
||||
$value = $this->punyEncodeName($_value);
|
||||
$value = $this->punyEncodeName($_value, $options['punycodeVersion']);
|
||||
// if result stayed within our allowed character limit, then good, we're done
|
||||
if(strlen($value) <= $maxLength) break;
|
||||
// continue loop until encoded value is equal or less than allowed max length
|
||||
$_value = substr($_value, 0, strlen($_value) - 1);
|
||||
$_value = $tt->substr($_value, 0, $max--);
|
||||
} while(true);
|
||||
|
||||
// if encode was necessary and successful, return with no further processing
|
||||
if(strpos($value, 'xn-') === 0) {
|
||||
return $value;
|
||||
} else {
|
||||
if(strlen($value) && ctype_alnum(str_replace(array('-', '_', '.'), '', $value))) {
|
||||
if($this->getPunycodeVersion($options['punycodeVersion']) > 1) return $value;
|
||||
}
|
||||
|
||||
// can't be encoded, send to regular name sanitizer
|
||||
$value = $_value;
|
||||
}
|
||||
@@ -842,7 +863,7 @@ class Sanitizer extends Wire {
|
||||
$beautify = self::okUTF8;
|
||||
if(strpos($value, 'xn-') === 0) {
|
||||
// found something to convert
|
||||
$value = $this->punyDecodeName($value);
|
||||
$value = $this->punyDecodeName($value, $options['punycodeVersion']);
|
||||
// now it will run through okUTF8
|
||||
}
|
||||
}
|
||||
@@ -893,6 +914,7 @@ class Sanitizer extends Wire {
|
||||
if(!strlen($value)) return '';
|
||||
|
||||
$config = $this->wire()->config;
|
||||
$keepGoing = true;
|
||||
|
||||
// if UTF8 module is not enabled then delegate this call to regular pageName sanitizer
|
||||
if($config->pageNameCharset != 'UTF8') return $this->pageName($value, false, $maxLength);
|
||||
@@ -908,7 +930,8 @@ class Sanitizer extends Wire {
|
||||
// whitelist of allowed characters and blacklist of disallowed characters
|
||||
$whitelist = $config->pageNameWhitelist;
|
||||
if(!strlen($whitelist)) $whitelist = false;
|
||||
$blacklist = '/\\%"\'<>?#@:;,+=*^$()[]{}|&';
|
||||
|
||||
$value = str_replace($this->pageNameBlacklist, '-', $value);
|
||||
|
||||
// we let regular pageName handle chars like these, if they appear without other UTF-8
|
||||
$extras = array('.', '-', '_', ',', ';', ':', '(', ')', '!', '?', '&', '%', '$', '#', '@');
|
||||
@@ -923,43 +946,48 @@ class Sanitizer extends Wire {
|
||||
if($this->caches[$k] || $tt->strtolower($value) === $value) {
|
||||
// whitelist supports only lowercase OR value is all lowercase
|
||||
// let regular pageName sanitizer handle this
|
||||
return $this->pageName($value, false, $maxLength);
|
||||
$value = $this->pageName($value, false, $maxLength);
|
||||
// maintain old behavior for existing installations
|
||||
if($this->getPunycodeVersion() < 2) return $value;
|
||||
$keepGoing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// validate that all characters are in our whitelist
|
||||
$replacements = array();
|
||||
if($keepGoing) {
|
||||
// validate that all characters are in our whitelist
|
||||
$replacements = array();
|
||||
|
||||
for($n = 0; $n < $tt->strlen($value); $n++) {
|
||||
$c = $tt->substr($value, $n, 1);
|
||||
$inBlacklist = $tt->strpos($blacklist, $c) !== false || strpos($blacklist, $c) !== false;
|
||||
$inWhitelist = !$inBlacklist && $whitelist !== false && $tt->strpos($whitelist, $c) !== false;
|
||||
if($inWhitelist && !$inBlacklist) {
|
||||
// in whitelist
|
||||
} else if($inBlacklist || !strlen(trim($c)) || ctype_cntrl($c)) {
|
||||
// character does not resolve to something visible or is in blacklist
|
||||
$replacements[] = $c;
|
||||
} else if($whitelist === false) {
|
||||
// whitelist disabled: allow everything that is not blacklisted
|
||||
} else {
|
||||
// character that is not in whitelist, double check case variants
|
||||
$cLower = $tt->strtolower($c);
|
||||
$cUpper = $tt->strtoupper($c);
|
||||
if($cLower !== $c && $tt->strpos($whitelist, $cLower) !== false) {
|
||||
// allow character and convert to lowercase variant
|
||||
$value = $tt->substr($value, 0, $n) . $cLower . $tt->substr($value, $n+1);
|
||||
} else if($cUpper !== $c && $tt->strpos($whitelist, $cUpper) !== false) {
|
||||
// allow character and convert to uppercase varient
|
||||
$value = $tt->substr($value, 0, $n) . $cUpper . $tt->substr($value, $n+1);
|
||||
} else {
|
||||
// queue character to be replaced
|
||||
for($n = 0; $n < $tt->strlen($value); $n++) {
|
||||
$c = $tt->substr($value, $n, 1);
|
||||
if($c === '-') continue;
|
||||
$inWhitelist = $whitelist !== false && $tt->strpos($whitelist, $c) !== false;
|
||||
if($inWhitelist) {
|
||||
// in whitelist
|
||||
} else if(!strlen(trim($c)) || ctype_cntrl($c)) {
|
||||
// character does not resolve to something visible
|
||||
$replacements[] = $c;
|
||||
} else if($whitelist === false) {
|
||||
// whitelist disabled: allow everything that is not blacklisted
|
||||
} else {
|
||||
// character that is not in whitelist, double check case variants
|
||||
$cLower = $tt->strtolower($c);
|
||||
$cUpper = $tt->strtoupper($c);
|
||||
if($cLower !== $c && $tt->strpos($whitelist, $cLower) !== false) {
|
||||
// allow character and convert to lowercase variant
|
||||
$value = $tt->substr($value, 0, $n) . $cLower . $tt->substr($value, $n + 1);
|
||||
} else if($cUpper !== $c && $tt->strpos($whitelist, $cUpper) !== false) {
|
||||
// allow character and convert to uppercase variant
|
||||
$value = $tt->substr($value, 0, $n) . $cUpper . $tt->substr($value, $n + 1);
|
||||
} else {
|
||||
// queue character to be replaced
|
||||
$replacements[] = $c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replace disallowed characters with "-"
|
||||
if(count($replacements)) $value = str_replace($replacements, '-', $value);
|
||||
// replace disallowed characters with "-"
|
||||
if(count($replacements)) $value = str_replace($replacements, '-', $value);
|
||||
}
|
||||
|
||||
// replace doubled word separators
|
||||
foreach($separators as $c) {
|
||||
@@ -980,36 +1008,51 @@ class Sanitizer extends Wire {
|
||||
* Decode a PW-punycode'd name value
|
||||
*
|
||||
* @param string $value
|
||||
* @param int $version 0=auto-detect, 1=original/buggy, 2=punycode library, 3=php idn function
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
protected function punyDecodeName($value) {
|
||||
protected function punyDecodeName($value, $version = 0) {
|
||||
// exclude values that we know can't be converted
|
||||
if(strlen($value) < 4 || strpos($value, 'xn-') !== 0) return $value;
|
||||
$version = $this->getPunycodeVersion($version);
|
||||
|
||||
if(strpos($value, '__')) {
|
||||
// as used by punycode version 1 to split long strings
|
||||
$_value = $value;
|
||||
$parts = explode('__', $_value);
|
||||
foreach($parts as $n => $part) {
|
||||
$parts[$n] = $this->punyDecodeName($part);
|
||||
$parts[$n] = $this->punyDecodeName($part, $version);
|
||||
}
|
||||
$value = implode('', $parts);
|
||||
return $value;
|
||||
}
|
||||
|
||||
$_value = $value;
|
||||
|
||||
// convert "xn-" single hyphen to recognized punycode "xn--" double hyphen
|
||||
if(strpos($value, 'xn--') !== 0) $value = 'xn--' . substr($value, 3);
|
||||
if(function_exists('idn_to_utf8')) {
|
||||
// use native php function if available
|
||||
$value = @idn_to_utf8($value);
|
||||
} else {
|
||||
// otherwise use Punycode class
|
||||
|
||||
if($version >= 3) {
|
||||
// PHP IDN function
|
||||
// 32=IDNA_NONTRANSITIONAL_TO_UNICODE
|
||||
$info = array();
|
||||
$value = idn_to_utf8($value, 32, INTL_IDNA_VARIANT_UTS46, $info);
|
||||
if(empty($value)) $value = $info['result'];
|
||||
|
||||
} else if($version === 2) {
|
||||
// Punycode library
|
||||
$pc = new Punycode();
|
||||
$value = $pc->decode($value);
|
||||
|
||||
} else {
|
||||
// PHP IDN with old/buggy behavior post PHP 7.4
|
||||
$value = @idn_to_utf8($value);
|
||||
}
|
||||
|
||||
// if utf8 conversion failed, restore original value
|
||||
if($value === false || !strlen($value)) $value = $_value;
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
@@ -1017,41 +1060,92 @@ class Sanitizer extends Wire {
|
||||
* Encode a name value to PW-punycode
|
||||
*
|
||||
* @param string $value
|
||||
* @param int $version 0=auto-detect, 1=original/buggy, 2=punycode library, 3=php idn function
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
protected function punyEncodeName($value) {
|
||||
// exclude values that don't need to be converted
|
||||
if(strpos($value, 'xn-') === 0) return $value;
|
||||
if(ctype_alnum(str_replace(array('.', '-', '_'), '', $value))) return $value;
|
||||
protected function punyEncodeName($value, $version = 0) {
|
||||
|
||||
$tt = $this->getTextTools();
|
||||
$version = $this->getPunycodeVersion($version);
|
||||
|
||||
if(strpos($value, 'xn-') === 0) {
|
||||
if(ctype_alnum(str_replace(array('.', '-', '_'), '', $value))) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
if($version > 1) {
|
||||
$whitelist = $this->wire()->config->pageNameWhitelist;
|
||||
$value = str_replace($this->pageNameBlacklist, '-', $value);
|
||||
$v = '';
|
||||
for($n = 0; $n < $tt->strlen($value); $n++) {
|
||||
$c = $tt->substr($value, $n, 1);
|
||||
if($tt->stripos($whitelist, $c) === false) {
|
||||
$c = $this->pageName($c, self::translate);
|
||||
if(empty($c) || $tt->stripos($whitelist, $c) === false) {
|
||||
$c = '-';
|
||||
}
|
||||
}
|
||||
$v .= $c;
|
||||
}
|
||||
while(strpos($v, '--') !== false) $v = str_replace('--', '-', $v);
|
||||
$value = $tt->trim($v, '-');
|
||||
}
|
||||
|
||||
if(ctype_alnum(str_replace(array('.', '-', '_'), '', $value))) {
|
||||
$value = $this->pageName(trim($value), true);
|
||||
return $value;
|
||||
}
|
||||
|
||||
while(strpos($value, '__') !== false) {
|
||||
$value = str_replace('__', '_', $value);
|
||||
}
|
||||
|
||||
if(strlen($value) >= 50) {
|
||||
|
||||
if($version > 1) {
|
||||
// version 2, 3
|
||||
while(strpos($value, '--') !== false) {
|
||||
$value = str_replace('--', '-', $value);
|
||||
}
|
||||
$value = trim($value, '-');
|
||||
|
||||
} else if(strlen($value) >= 50) {
|
||||
// version 1
|
||||
$_value = $value;
|
||||
$parts = array();
|
||||
while(strlen($_value)) {
|
||||
$part = $tt->substr($_value, 0, 12);
|
||||
$_value = $tt->substr($_value, 12);
|
||||
$parts[] = $this->punyEncodeName($part);
|
||||
$parts[] = $this->punyEncodeName($part, $version);
|
||||
}
|
||||
$value = implode('__', $parts);
|
||||
return $value;
|
||||
return $value;
|
||||
}
|
||||
|
||||
|
||||
$_value = $value;
|
||||
|
||||
if(function_exists("idn_to_ascii")) {
|
||||
// use native php function if available
|
||||
$value = substr(@idn_to_ascii($value), 3);
|
||||
} else {
|
||||
// otherwise use Punycode class
|
||||
|
||||
if($version >= 3) {
|
||||
// PHP 7.4+ idn_to_ascii
|
||||
$info = array();
|
||||
// 16=IDNA_NONTRANSITIONAL_TO_ASCII
|
||||
idn_to_ascii($value, 16, INTL_IDNA_VARIANT_UTS46, $info);
|
||||
// IDN return value fails on longer strings, but populates result correctly
|
||||
$value = $info['result'];
|
||||
|
||||
} else if($version === 2) {
|
||||
// Punycode library
|
||||
$pc = new Punycode();
|
||||
$value = substr($pc->encode($value), 3);
|
||||
$value = $pc->encode($value);
|
||||
|
||||
} else {
|
||||
// buggy behavior in PHP 7.4+ but pages may already be present with it
|
||||
// INTL_IDNA_VARIANT_2003 is default prior to PHP 7.4
|
||||
// substr() is also not right here but kept for v1 compatibility
|
||||
$value = substr(@idn_to_ascii($value), 3);
|
||||
}
|
||||
|
||||
if(strpos($value, 'xn-') === 0) $value = substr($value, 3);
|
||||
|
||||
if(strlen($value) && $value !== '-') {
|
||||
// in PW the xn- prefix has one fewer hyphen than in native Punycode
|
||||
// for compatibility with pageName sanitization and beautification
|
||||
@@ -1061,8 +1155,45 @@ class Sanitizer extends Wire {
|
||||
// return value is always ascii
|
||||
$value = $this->name($_value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get internal Punycode version to use
|
||||
*
|
||||
* 0: Auto-detect from current environment.
|
||||
* 1: PHP IDN function used by all PW versions prior to 3.0.244, but buggy PHP 7.4+.
|
||||
* 2: Dedicated Punycode PHP library (no known issues at present).
|
||||
* 3: PHP IDN function call updated for PHP 7.4+ (default in new installations after January 2025).
|
||||
*
|
||||
* @param int $version
|
||||
* @return int 1=PHP DN but buggy after PHP 7.4+, 2=Punycode library, 3=PHP IDN function PHP 7.4+
|
||||
* @since 3.0.244
|
||||
*
|
||||
*/
|
||||
protected function getPunycodeVersion($version = 0) {
|
||||
$config = $this->wire()->config;
|
||||
if(!$version) {
|
||||
$whitelist = $config->pageNameWhitelist;
|
||||
for($n = 3; $n > 0; $n--) {
|
||||
if(strpos($whitelist, "v$n") !== false) $version = $n;
|
||||
if($version) break;
|
||||
}
|
||||
}
|
||||
if(!$version) $version = $config->installedAfter('2025-01-10') ? 3 : 1;
|
||||
if(!function_exists('idn_to_utf8')) $version = 2;
|
||||
if($version >= 3 && version_compare(phpversion(), '7.4.0', '<')) $version = 2;
|
||||
return $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Punycode
|
||||
*
|
||||
*/
|
||||
protected function punycode() {
|
||||
return new Punycode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format required by ProcessWire user names
|
||||
@@ -1082,7 +1213,17 @@ class Sanitizer extends Wire {
|
||||
* Name filter for ProcessWire filenames (basenames only, not paths)
|
||||
*
|
||||
* This sanitizes a filename to be consistent with the name format in ProcessWire,
|
||||
* ASCII-alphanumeric, hyphens, underscores and periods.
|
||||
* ASCII-alphanumeric (a-z A-Z 0-9), hyphens, underscores and periods. Note that
|
||||
* filenames may contain mixed case (a-z A-Z) so if you require lowercase then
|
||||
* run the return value through a `strtolower()` function.
|
||||
*
|
||||
* ~~~~~
|
||||
* // outputs: FileName.jpg
|
||||
* echo $sanitizer->filename('©®™FileName.jpg');
|
||||
*
|
||||
* // outputs: c_r_tmfilename.jpg
|
||||
* echo strtolower($sanitizer->filename('©®™filename.jpg', Sanitizer::translate));
|
||||
* ~~~~~
|
||||
*
|
||||
* #pw-group-strings
|
||||
* #pw-group-files
|
||||
@@ -1102,9 +1243,10 @@ class Sanitizer extends Wire {
|
||||
|
||||
if(strlen($value) > $maxLength) {
|
||||
// truncate, while keeping extension in tact
|
||||
$tt = $this->getTextTools();
|
||||
$pathinfo = pathinfo($value);
|
||||
$extLen = strlen($pathinfo['extension']) + 1; // +1 includes period
|
||||
$basename = substr($pathinfo['filename'], 0, $maxLength - $extLen);
|
||||
$extLen = $tt->strlen($pathinfo['extension']) + 1; // +1 includes period
|
||||
$basename = $tt->substr($pathinfo['filename'], 0, $maxLength - $extLen);
|
||||
$value = "$basename.$pathinfo[extension]";
|
||||
}
|
||||
|
||||
@@ -1983,7 +2125,14 @@ class Sanitizer extends Wire {
|
||||
$value = preg_replace('!</li>\s*<li!is', "$options[separator]<li", $value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// replace single less than sign that's not accompanied with a greater than sign
|
||||
// to something that looks like it, but that strip_tags() won’t strip.
|
||||
// this is to prevent something like "5<10" from getting converted to "5"
|
||||
if(strpos($value, '<') !== false && strpos($value, '>') === false) {
|
||||
$value = preg_replace('/<([\w\d])/', '≺$1', $value);
|
||||
}
|
||||
|
||||
// remove tags
|
||||
$value = trim(strip_tags($value));
|
||||
|
||||
@@ -2363,7 +2512,7 @@ class Sanitizer extends Wire {
|
||||
|
||||
} else {
|
||||
// domain contains utf8
|
||||
$pc = function_exists("idn_to_ascii") ? false : new Punycode();
|
||||
$pc = function_exists("idn_to_ascii") ? false : $this->punycode();
|
||||
$domain = $pc ? $pc->encode($domain) : @idn_to_ascii($domain);
|
||||
if($domain === false || !strlen($domain)) return '';
|
||||
$url = $scheme . $domain . $rest;
|
||||
@@ -4431,6 +4580,7 @@ class Sanitizer extends Wire {
|
||||
* - `delimiter` (string): Single delimiter to use to identify CSV strings. Overrides the 'delimiters' option when specified (default=null)
|
||||
* - `delimiters` (array): Delimiters to identify CSV strings. First found delimiter will be used, default=array("|", ",")
|
||||
* - `enclosure` (string): Enclosure to use for CSV strings (default=double quote, i.e. `"`)
|
||||
* - `escape` (string): Escape to use for CSV strings (default=backslash, i.e. "\\")
|
||||
* @return array
|
||||
* @throws WireException if an unknown $sanitizer method is given
|
||||
*
|
||||
@@ -4446,6 +4596,7 @@ class Sanitizer extends Wire {
|
||||
'delimiter' => null,
|
||||
'delimiters' => array('|', ','),
|
||||
'enclosure' => '"',
|
||||
'escape' => "\\",
|
||||
'trim' => true,
|
||||
'sanitizer' => null,
|
||||
'keySanitizer' => null,
|
||||
@@ -4484,7 +4635,7 @@ class Sanitizer extends Wire {
|
||||
}
|
||||
}
|
||||
if($hasDelimiter !== null) {
|
||||
$value = str_getcsv($value, $hasDelimiter, $options['enclosure']);
|
||||
$value = str_getcsv($value, $hasDelimiter, $options['enclosure'], $options['escape']);
|
||||
} else {
|
||||
$value = array($value);
|
||||
}
|
||||
@@ -4862,6 +5013,7 @@ class Sanitizer extends Wire {
|
||||
* - `maxWordLength` (int): Maximum word length (default=80)
|
||||
* - `maxWords` (int): Maximum number of words allowed (default=0, no limit)
|
||||
* - `stripTags` (bool): Strip markup tags so they don’t contribute to returned word list? (default=true)
|
||||
* - `truncate` (bool): Truncate rather than remove words that exceed maxWordLength? (default=false) 3.0.250+
|
||||
* @return array
|
||||
* @since 3.0.160
|
||||
*
|
||||
@@ -4879,6 +5031,7 @@ class Sanitizer extends Wire {
|
||||
'keepNumberFormat' => true,
|
||||
'keepChars' => array(),
|
||||
'stripTags' => true,
|
||||
'truncate' => false,
|
||||
);
|
||||
|
||||
$options = array_merge($defaults, $options);
|
||||
@@ -4971,8 +5124,17 @@ class Sanitizer extends Wire {
|
||||
continue;
|
||||
}
|
||||
$length = $this->multibyteSupport ? mb_strlen($word) : strlen($word);
|
||||
if($length < $minLength || $length > $maxLength) {
|
||||
// remove any words that are outside the min/max length requirements
|
||||
if($length > $maxLength) {
|
||||
// remove or truncate any words that are too long
|
||||
if($options['truncate']) {
|
||||
$word = $this->multibyteSupport ? mb_substr($word, 0, $maxLength) : substr($word, 0, $maxLength);
|
||||
$words[$key] = $word;
|
||||
} else {
|
||||
unset($words[$key]);
|
||||
continue;
|
||||
}
|
||||
} else if($length < $minLength) {
|
||||
// remove any words that are are not long enough
|
||||
unset($words[$key]);
|
||||
continue;
|
||||
} else if($keepChars !== '' && !strlen(trim($word, $keepChars))) {
|
||||
@@ -5177,7 +5339,7 @@ class Sanitizer extends Wire {
|
||||
* @param string|int|array|float $value
|
||||
* @param int $maxLength Maximum length (default=128)
|
||||
* @param null|int $maxBytes Maximum allowed bytes (used for string types only)
|
||||
* @return array|bool|float|int|string
|
||||
* @return array|float|int|string
|
||||
* @since 3.0.125
|
||||
* @see Sanitizer::minLength()
|
||||
*
|
||||
|
@@ -109,6 +109,8 @@ class Selectors extends WireArray {
|
||||
*/
|
||||
public function __construct($selector = null) {
|
||||
parent::__construct();
|
||||
$this->usesNumericKeys = false;
|
||||
$this->indexedByName = false;
|
||||
if(!is_null($selector)) $this->init($selector);
|
||||
}
|
||||
|
||||
@@ -771,38 +773,99 @@ class Selectors extends WireArray {
|
||||
* @param Wire $item
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
*/
|
||||
public function matches(Wire $item) {
|
||||
|
||||
// if item provides it's own matches function, then let it have control
|
||||
|
||||
// if item provides it's own matches function (like Page), then let it have control
|
||||
if($item instanceof WireMatchable) return $item->matches($this);
|
||||
|
||||
|
||||
$orGroups = array();
|
||||
$matches = true;
|
||||
|
||||
foreach($this as $selector) {
|
||||
$value = array();
|
||||
foreach($selector->fields as $property) {
|
||||
if(strpos($property, '.') && $item instanceof WireData) {
|
||||
$value[] = $item->getDot($property);
|
||||
} else {
|
||||
$value[] = (string) $item->$property;
|
||||
}
|
||||
}
|
||||
if(!$selector->matches($value)) {
|
||||
$matches = false;
|
||||
// attempt any alternate operators, if present
|
||||
foreach($selector->altOperators as $altOperator) {
|
||||
$altSelector = self::getSelectorByOperator($altOperator);
|
||||
if(!$altSelector) continue;
|
||||
$this->wire($altSelector);
|
||||
$selector->copyTo($altSelector);
|
||||
$matches = $altSelector->matches($value);
|
||||
if($matches) break;
|
||||
}
|
||||
// if neither selector nor altSelectors match then stop
|
||||
if($selector->quote === '(' && self::stringHasOperator($selector->value())) {
|
||||
$name = $selector->field();
|
||||
if(!isset($orGroups[$name])) $orGroups[$name] = array();
|
||||
$orGroups[$name][] = $selector->value;
|
||||
} else {
|
||||
$matches = $this->matchesSelector($selector, $item);
|
||||
if(!$matches) break;
|
||||
}
|
||||
}
|
||||
|
||||
if($matches && count($orGroups)) {
|
||||
$matches = $this->matchesOrGroups($orGroups, $item);
|
||||
}
|
||||
|
||||
return $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the given Wire match these Selector (single)?
|
||||
*
|
||||
* @param Selector $selector
|
||||
* @param Wire $item
|
||||
* @return bool
|
||||
* @since 3.0.330
|
||||
*
|
||||
*/
|
||||
protected function matchesSelector(Selector $selector, Wire $item) {
|
||||
$value = array();
|
||||
|
||||
foreach($selector->fields as $property) {
|
||||
if(strpos($property, '.') && $item instanceof WireData) {
|
||||
$v = $item->getDot($property);
|
||||
} else {
|
||||
$v = $item->$property;
|
||||
}
|
||||
if(is_array($v)) {
|
||||
$value = array_merge($value, $v);
|
||||
} else {
|
||||
$value[] = (string) $v;
|
||||
}
|
||||
}
|
||||
|
||||
$matches = $selector->matches($value);
|
||||
if($matches) return true;
|
||||
|
||||
// attempt any alternate operators, if present
|
||||
foreach($selector->altOperators as $altOperator) {
|
||||
$altSelector = self::getSelectorByOperator($altOperator);
|
||||
if(!$altSelector) continue;
|
||||
$this->wire($altSelector);
|
||||
$selector->copyTo($altSelector);
|
||||
$matches = $altSelector->matches($value);
|
||||
if($matches) break;
|
||||
}
|
||||
|
||||
return $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the given OR-groups match the given Wire?
|
||||
*
|
||||
* @param array|string[]|array[] $orGroups
|
||||
* @param Wire $item
|
||||
* @return bool
|
||||
* @since 3.0.330
|
||||
*
|
||||
*/
|
||||
protected function matchesOrGroups(array $orGroups, Wire $item) {
|
||||
$matches = true;
|
||||
foreach($orGroups as $selectorStrings) {
|
||||
$orGroupMatches = false;
|
||||
foreach($selectorStrings as $s) {
|
||||
/** @var Selectors $orGroupSelectors */
|
||||
$orGroupSelectors = $this->wire(new Selectors($s));
|
||||
if(!$orGroupSelectors->matches($item)) continue;
|
||||
$orGroupMatches = true;
|
||||
break;
|
||||
}
|
||||
if(!$orGroupMatches) {
|
||||
$matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $matches;
|
||||
}
|
||||
|
||||
|
@@ -381,7 +381,7 @@ class Session extends Wire implements \IteratorAggregate {
|
||||
// if valid, update last request time
|
||||
$this->set('_user', 'ts', time());
|
||||
|
||||
} else if($reason && $userID && $userID != $this->wire('config')->guestUserPageID) {
|
||||
} else if($reason && $userID && $userID != $this->config->guestUserPageID) {
|
||||
// otherwise log the invalid session
|
||||
$user = $this->wire()->users->get((int) $userID);
|
||||
if($user && $user->id) $reason = "User '$user->name' - $reason";
|
||||
@@ -1706,7 +1706,7 @@ class Session extends Wire implements \IteratorAggregate {
|
||||
* @since 3.0.166
|
||||
*
|
||||
*/
|
||||
public function sessionHandler(WireSessionHandler $sessionHandler = null) {
|
||||
public function sessionHandler(?WireSessionHandler $sessionHandler = null) {
|
||||
if($sessionHandler) $this->sessionHandler = $sessionHandler;
|
||||
return $this->sessionHandler;
|
||||
}
|
||||
|
@@ -59,8 +59,8 @@
|
||||
* @property string $sortfield Field that children of templates using this page should sort by (leave blank to let page decide, or specify "sort" for manual drag-n-drop). #pw-group-family
|
||||
* @property int $noChildren Set to 1 to cancel use of childTemplates. #pw-group-family
|
||||
* @property int $noParents Set to 1 to cancel use of parentTemplates, set to -1 to only allow one page using this template to exist. #pw-group-family
|
||||
* @property array $childTemplates Array of template IDs that are allowed for children. Blank array indicates "any". #pw-group-family
|
||||
* @property array $parentTemplates Array of template IDs that are allowed for parents. Blank array indicates "any". #pw-group-family
|
||||
* @property int[] $childTemplates Array of template IDs that are allowed for children. Blank array indicates "any". #pw-group-family
|
||||
* @property int[] $parentTemplates Array of template IDs that are allowed for parents. Blank array indicates "any". #pw-group-family
|
||||
* @property string $childNameFormat Name format for child pages. when specified, the page-add UI step can be skipped when adding children. Counter appended till unique. Date format assumed if any non-pageName chars present. Use 'title' to pull from title field. #pw-group-family
|
||||
*
|
||||
* URLs
|
||||
@@ -1171,7 +1171,6 @@ class Template extends WireData implements Saveable, Exportable {
|
||||
* Given an array of export data, import it
|
||||
*
|
||||
* @param array $data
|
||||
* @return bool True if successful, false if not
|
||||
* @return array Returns array(
|
||||
* [property_name] => array(
|
||||
* 'old' => 'old value', // old value (in string comparison format)
|
||||
@@ -1222,7 +1221,7 @@ class Template extends WireData implements Saveable, Exportable {
|
||||
* #pw-group-family
|
||||
*
|
||||
* @param array|TemplatesArray|null $setValue Specify only when setting, an iterable value containing Template objects, IDs or names
|
||||
* @return TemplatesArray
|
||||
* @return TemplatesArray|Template[]
|
||||
* @since 3.0.153
|
||||
*
|
||||
*/
|
||||
@@ -1237,7 +1236,7 @@ class Template extends WireData implements Saveable, Exportable {
|
||||
*
|
||||
* @param string $property Specify either 'childTemplates' or 'parentTemplates'
|
||||
* @param array|TemplatesArray|null $setValue Iterable value containing Template objects, IDs or names
|
||||
* @return TemplatesArray
|
||||
* @return TemplatesArray|Template[]
|
||||
* @since 3.0.153
|
||||
*
|
||||
*/
|
||||
@@ -1267,6 +1266,7 @@ class Template extends WireData implements Saveable, Exportable {
|
||||
if($template) $value->add($template);
|
||||
}
|
||||
}
|
||||
/** @var TemplatesArray|Template[] $value */
|
||||
|
||||
return $value;
|
||||
}
|
||||
@@ -1299,8 +1299,8 @@ class Template extends WireData implements Saveable, Exportable {
|
||||
* This is based on family settings, when applicable.
|
||||
* It also takes into account user access, if requested (see arg 1).
|
||||
*
|
||||
* If there is no shortcut parent, NULL is returned.
|
||||
* If there are multiple possible shortcut parents, a NullPage is returned.
|
||||
* If there is no defined parent, NULL is returned.
|
||||
* If there are multiple defined parents, a NullPage is returned.
|
||||
*
|
||||
* #pw-group-family
|
||||
*
|
||||
@@ -1313,7 +1313,7 @@ class Template extends WireData implements Saveable, Exportable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all possible parent pages for this template
|
||||
* Return all defined parent pages for this template
|
||||
*
|
||||
* #pw-group-family
|
||||
*
|
||||
@@ -1617,5 +1617,3 @@ class Template extends WireData implements Saveable, Exportable {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@@ -609,14 +609,20 @@ class TemplateFile extends WireData {
|
||||
*
|
||||
* USAGE from template file is: return $this->halt();
|
||||
*
|
||||
* @param bool $halt
|
||||
* @param bool|string $halt
|
||||
* If given boolean, it will set the halt status.
|
||||
* If given string, it will be output (3.0.239+)
|
||||
* @return $this
|
||||
*
|
||||
*/
|
||||
protected function halt($halt = true) {
|
||||
$this->halt = $halt ? true : false;
|
||||
public function halt($halt = true) {
|
||||
if(is_bool($halt)) {
|
||||
$this->halt = $halt ? true : false;
|
||||
} else if(is_string($halt)) {
|
||||
$this->halt = true;
|
||||
echo $halt;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
*
|
||||
* Manages and provides access to all the Template instances
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* #pw-summary Manages and provides access to all the Templates.
|
||||
@@ -693,8 +693,8 @@ class Templates extends WireSaveableItems {
|
||||
*
|
||||
* - This is based on family settings, when applicable.
|
||||
* - It also takes into account user access, if requested (see arg 1).
|
||||
* - If there is no shortcut parent, NULL is returned.
|
||||
* - If there are multiple possible shortcut parents, a NullPage is returned.
|
||||
* - If there is no defined parent, NULL is returned.
|
||||
* - If there are multiple defined parents, a NullPage is returned (use $getAll to get them).
|
||||
*
|
||||
* @param Template $template
|
||||
* @param bool $checkAccess Whether or not to check for user access to do this (default=false).
|
||||
@@ -706,84 +706,63 @@ class Templates extends WireSaveableItems {
|
||||
public function getParentPage(Template $template, $checkAccess = false, $getAll = false) {
|
||||
|
||||
$pages = $this->wire()->pages;
|
||||
$user = $this->wire()->user;
|
||||
|
||||
$foundParent = null;
|
||||
$foundParents = $getAll ? $pages->newPageArray() : null;
|
||||
$foundParentQty = 0;
|
||||
$foundParents = $pages->newPageArray();
|
||||
$maxStatus = is_int($getAll) && $getAll ? ($getAll * 2) : 0;
|
||||
$earlyExit = false;
|
||||
|
||||
if($template->noShortcut || !count($template->parentTemplates)) return $foundParents;
|
||||
if($template->noParents == -1) {
|
||||
// only 1 page of this type allowed
|
||||
if($this->getNumPages($template) > 0) return $foundParents;
|
||||
if($this->getNumPages($template) > 0) $earlyExit = true;
|
||||
} else if($template->noParents == 1) {
|
||||
return $foundParents;
|
||||
// no parents allowed
|
||||
$earlyExit = true;
|
||||
} else if(!count($template->parentTemplates)) {
|
||||
// no parent templates defined
|
||||
$earlyExit = true;
|
||||
}
|
||||
|
||||
if($earlyExit) return $getAll ? $foundParents : null;
|
||||
|
||||
$childTestPage = $checkAccess ? $pages->newPage($template) : null;
|
||||
|
||||
foreach($template->parentTemplates as $parentTemplateID) {
|
||||
|
||||
$parentTemplate = $this->get((int) $parentTemplateID);
|
||||
if(!$parentTemplate) continue;
|
||||
|
||||
// if parent template does not exist or not allow children, skip it
|
||||
if(!$parentTemplate || $parentTemplate->noChildren) continue;
|
||||
|
||||
// if the parent template doesn't have this as an allowed child template, exclude it
|
||||
if($parentTemplate->noChildren) continue;
|
||||
// if the parent template doesn't have this as an allowed child template, skip it
|
||||
if(!in_array($template->id, $parentTemplate->childTemplates)) continue;
|
||||
|
||||
// sort=status ensures that a non-hidden page is given preference to a hidden page
|
||||
$include = $checkAccess ? "unpublished" : "all";
|
||||
$selector = "templates_id=$parentTemplate->id, include=$include, sort=status";
|
||||
|
||||
if($maxStatus) {
|
||||
$selector .= ", status<$maxStatus";
|
||||
} else if(!$getAll) {
|
||||
} else if(!$getAll && !$checkAccess) {
|
||||
$selector .= ", limit=2";
|
||||
}
|
||||
$parentPages = $pages->find($selector);
|
||||
$numParentPages = count($parentPages);
|
||||
|
||||
// undetermined parent
|
||||
if(!$numParentPages) continue;
|
||||
|
||||
if($getAll) {
|
||||
// build list of all parents (will check access outside loop)
|
||||
$foundParents->add($parentPages);
|
||||
continue;
|
||||
} else if($numParentPages > 1) {
|
||||
// multiple possible parents, we can early-exit
|
||||
$foundParentQty += $numParentPages;
|
||||
break;
|
||||
} else {
|
||||
// one possible parent
|
||||
$parentPage = $parentPages->first();
|
||||
|
||||
foreach($pages->find($selector) as $parentPage) {
|
||||
if($checkAccess && !$parentPage->addable($childTestPage)) continue;
|
||||
$foundParents->add($parentPage);
|
||||
$earlyExit = !$getAll && $foundParents->count() > 1;
|
||||
if($earlyExit) break;
|
||||
}
|
||||
|
||||
if($checkAccess) {
|
||||
if($parentPage->id) {
|
||||
// single defined parent
|
||||
$p = $pages->newPage($template);
|
||||
if(!$parentPage->addable($p)) continue;
|
||||
} else {
|
||||
// multiple possible parents
|
||||
if(!$user->hasPermission('page-create', $template)) continue;
|
||||
}
|
||||
}
|
||||
|
||||
if($parentPage && $parentPage->id) $foundParentQty++;
|
||||
$foundParent = $parentPage;
|
||||
if($foundParentQty > 1) break;
|
||||
|
||||
if($earlyExit) break;
|
||||
}
|
||||
|
||||
if($checkAccess && $getAll && $foundParents && $foundParents->count()) {
|
||||
$p = $pages->newPage($template);
|
||||
foreach($foundParents as $parentPage) {
|
||||
if(!$parentPage->addable($p)) $foundParents->remove($parentPage);
|
||||
}
|
||||
}
|
||||
if($getAll) return $foundParents; // always returns PageArray (populated or not)
|
||||
|
||||
if($getAll) return $foundParents;
|
||||
if($foundParentQty > 1) return $pages->newNullPage();
|
||||
$qty = $foundParents->count();
|
||||
if($qty > 1) return $pages->newNullPage(); // multiple possible parents
|
||||
if($qty === 1) return $foundParents->first(); // one possible parent
|
||||
|
||||
return $foundParent;
|
||||
return null; // no parents
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -872,11 +851,24 @@ class Templates extends WireSaveableItems {
|
||||
|
||||
// determine if custom class available (3.0.152+)
|
||||
if($usePageClasses) {
|
||||
// generate a CamelCase name + 'Page' from template name, i.e. 'blog-post' => 'BlogPostPage'
|
||||
$className = ucwords(str_replace(array('-', '_', '.'), ' ', $template->name));
|
||||
$className = __NAMESPACE__ . "\\" . str_replace(' ', '', $className) . 'Page';
|
||||
if(class_exists($className) && wireInstanceOf($className, $corePageClass)) {
|
||||
$pageClass = $className;
|
||||
$customPageClass = '';
|
||||
// repeaters support a field-name based name strategy
|
||||
/** @var RepeaterField $field */
|
||||
if(strpos($template->name, 'repeater_') === 0) {
|
||||
$field = $this->wire()->fields->get(ltrim(strstr($template->name, '_'), '_'));
|
||||
if($field && wireInstanceOf($field->type, 'FieldtypeRepeater')) {
|
||||
$customPageClass = $field->type->getCustomPageClass($field);
|
||||
}
|
||||
}
|
||||
if($customPageClass) {
|
||||
$pageClass = $customPageClass;
|
||||
} else {
|
||||
// generate a CamelCase name + 'Page' from template name, i.e. 'blog-post' => 'BlogPostPage'
|
||||
$className = ucwords(str_replace(array('-', '_', '.'), ' ', $template->name));
|
||||
$className = __NAMESPACE__ . "\\" . str_replace(' ', '', $className) . 'Page';
|
||||
if(class_exists($className) && wireInstanceOf($className, $corePageClass)) {
|
||||
$pageClass = $className;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1155,4 +1147,3 @@ class Templates extends WireSaveableItems {
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
*
|
||||
* WireArray of Template instances as used by Templates class.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*/
|
||||
@@ -21,6 +21,7 @@ class TemplatesArray extends WireArray {
|
||||
}
|
||||
|
||||
public function getItemKey($item) {
|
||||
/** @var Template $item */
|
||||
return $item->id;
|
||||
}
|
||||
|
||||
|
@@ -484,11 +484,11 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
||||
/**
|
||||
* Get the TFA module for given user or current session
|
||||
*
|
||||
* @param User $user Optionally specify user
|
||||
* @param User|null $user Optionally specify user
|
||||
* @return Tfa|null
|
||||
*
|
||||
*/
|
||||
public function getModule(User $user = null) {
|
||||
public function getModule(?User $user = null) {
|
||||
|
||||
$module = null;
|
||||
$moduleName = $this->sessionGet('type');
|
||||
@@ -599,6 +599,7 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
||||
$f->attr('name', 'tfa_code');
|
||||
$f->label = $this->inputLabel; // Authentication code
|
||||
$f->attr('required', 'required');
|
||||
$f->attr('autocomplete', 'one-time-code');
|
||||
$f->collapsed = Inputfield::collapsedNever;
|
||||
$form->add($f);
|
||||
|
||||
@@ -913,13 +914,13 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
||||
* Modules that support auto-enable must implement this method to return true. Modules
|
||||
* that do not support it can ignore this method, as the default returns false.
|
||||
*
|
||||
* @param User $user Specify user to also confirm it is supported for given user.
|
||||
* @param User|null $user Specify user to also confirm it is supported for given user.
|
||||
* Omit to test if the module supports it in general.
|
||||
* @return bool
|
||||
* @since 3.0.160
|
||||
*
|
||||
*/
|
||||
public function autoEnableSupported(User $user = null) {
|
||||
public function autoEnableSupported(?User $user = null) {
|
||||
if($user && $this->className() !== 'Tfa') {
|
||||
// if it doesn't support it without user, then exit now
|
||||
if(!$this->autoEnableSupported()) return false;
|
||||
@@ -1901,7 +1902,7 @@ class RememberTfa extends Wire {
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
protected function serverValue($cookieValue, User $user = null) {
|
||||
protected function serverValue($cookieValue, ?User $user = null) {
|
||||
if($user === null) $user = $this->user;
|
||||
return sha1(
|
||||
$user->id . $user->name . $user->email .
|
||||
@@ -1954,11 +1955,11 @@ class RememberTfa extends Wire {
|
||||
/**
|
||||
* Get fingerprint string
|
||||
*
|
||||
* @param array $types Fingerprints to use, or omit when creating new
|
||||
* @param array|null $types Fingerprints to use, or omit when creating new
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public function getFingerprintString(array $types = null) {
|
||||
public function getFingerprintString(?array $types = null) {
|
||||
if($types === null) $types = $this->fingerprints;
|
||||
return implode(',', $types) . ':' . sha1(implode("\n", $this->getFingerprintArray()));
|
||||
}
|
||||
|
@@ -41,10 +41,10 @@ class User extends Page {
|
||||
/**
|
||||
* Create a new User page in memory.
|
||||
*
|
||||
* @param Template $tpl Template object this page should use.
|
||||
* @param Template|null $tpl Template object this page should use.
|
||||
*
|
||||
*/
|
||||
public function __construct(Template $tpl = null) {
|
||||
public function __construct(?Template $tpl = null) {
|
||||
if(!$tpl) $this->template = $this->wire()->templates->get('user');
|
||||
$this->_parent_id = $this->wire()->config->usersPageID;
|
||||
parent::__construct($tpl);
|
||||
@@ -234,11 +234,11 @@ class User extends Page {
|
||||
* #pw-group-access
|
||||
*
|
||||
* @param string|Permission
|
||||
* @param Page $page Optional page to check against
|
||||
* @param Page|null $page Optional page to check against
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
protected function ___hasPagePermission($name, Page $page = null) {
|
||||
protected function ___hasPagePermission($name, ?Page $page = null) {
|
||||
|
||||
if($this->isSuperuser()) return true;
|
||||
$permissions = $this->wire()->permissions;
|
||||
@@ -404,11 +404,11 @@ class User extends Page {
|
||||
*
|
||||
* #pw-group-access
|
||||
*
|
||||
* @param Page $page Optional page to check against
|
||||
* @param Page|null $page Optional page to check against
|
||||
* @return PageArray of Permission objects
|
||||
*
|
||||
*/
|
||||
public function getPermissions(Page $page = null) {
|
||||
public function getPermissions(?Page $page = null) {
|
||||
// Does not currently include page-add or page-create permissions (runtime).
|
||||
if($this->isSuperuser()) return $this->wire()->permissions->getIterator(); // all permissions
|
||||
$userPermissions = $this->wire()->pages->newPageArray();
|
||||
|
@@ -14,6 +14,8 @@
|
||||
* @method void added($user) Hook called just after a User is added #pw-hooker
|
||||
* @method void deleteReady($user) Hook called before a User is deleted #pw-hooker
|
||||
* @method void deleted($user) Hook called after a User is deleted #pw-hooker
|
||||
* @method User new($options = []) Create new User instance in memory (3.0.249+)
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -156,9 +158,11 @@ class Users extends PagesType {
|
||||
*
|
||||
*/
|
||||
public function newUser() {
|
||||
$config = $this->wire()->config;
|
||||
/** @var User $user */
|
||||
$user = $this->wire()->pages->newPage(array(
|
||||
'template' => 'user',
|
||||
'template' => $this->wire()->templates->get($config->userTemplateID),
|
||||
'parent' => $config->usersPageID,
|
||||
'pageClass' => $this->getPageClass()
|
||||
));
|
||||
return $user;
|
||||
|
@@ -626,7 +626,7 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
|
||||
* @deprecated
|
||||
*
|
||||
*/
|
||||
static public function isHooked($method, Wire $instance = null) {
|
||||
static public function isHooked($method, ?Wire $instance = null) {
|
||||
/** @var ProcessWire $wire */
|
||||
$wire = $instance ? $instance->wire() : ProcessWire::getCurrentInstance();
|
||||
if($instance) return $instance->wire()->hooks->hasHook($instance, $method);
|
||||
@@ -1370,7 +1370,7 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
|
||||
*
|
||||
* #pw-hooker
|
||||
*
|
||||
* @param \Exception|WireException $e Exception object that was thrown.
|
||||
* @param \Exception $e Exception object that was thrown.
|
||||
* @param bool|int $severe Whether or not it should be considered severe (default=true).
|
||||
* @param string|array|object|true $text Additional details (optional):
|
||||
* - When provided, it will be sent to `$this->error($text)` if $severe is true, or `$this->warning($text)` if $severe is false.
|
||||
@@ -1758,7 +1758,7 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
|
||||
* @param string|object $name Name of API variable to retrieve, set, or omit to retrieve the master ProcessWire object.
|
||||
* @param null|mixed $value Value to set if using this as a setter, otherwise omit.
|
||||
* @param bool $lock When using as a setter, specify true if you want to lock the value from future changes (default=false).
|
||||
* @return ProcessWire|Wire|Session|Page|Pages|Modules|User|Users|Roles|Permissions|Templates|Fields|Fieldtypes|Sanitizer|Config|Notices|WireDatabasePDO|WireHooks|WireDateTime|WireFileTools|WireMailTools|WireInput|string|mixed
|
||||
* @return ProcessWire|Wire|Session|Page|Pages|Modules|User|Users|Roles|Permissions|Templates|Fields|Fieldtypes|Sanitizer|Config|Notices|WireDatabasePDO|WireHooks|WireDateTime|WireFileTools|WireMailTools|WireInput|PagesVersions|string|mixed
|
||||
* @throws WireException
|
||||
*
|
||||
*
|
||||
@@ -1912,4 +1912,3 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@@ -11,11 +11,10 @@
|
||||
*
|
||||
* @todo can we implement next() and prev() like on Page, as alias to getNext() and getPrev()?
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @method WireArray and($item)
|
||||
* @method static WireArray new($items = array())
|
||||
* @property int $count Number of items
|
||||
* @property Wire|null $first First item
|
||||
* @property Wire|null $last Last item
|
||||
@@ -88,12 +87,47 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
*/
|
||||
protected $sortFlags = 0; // 0 == SORT_REGULAR
|
||||
|
||||
/**
|
||||
* For WireArray that holds WireData objects, property that contains the item’s name
|
||||
*
|
||||
* @var string
|
||||
* @since 3.0.240
|
||||
*
|
||||
*/
|
||||
protected $nameProperty = 'name';
|
||||
|
||||
/**
|
||||
* Is this WireArray indexed by the name property?
|
||||
*
|
||||
* This will be auto-detected at runtime unless specifically set in the constructor.
|
||||
*
|
||||
* @var bool|null Bool once known, null if not yet known
|
||||
* @since 3.0.240
|
||||
*
|
||||
*/
|
||||
protected $indexedByName = null;
|
||||
|
||||
/**
|
||||
* Does this WireArray use numeric keys?
|
||||
*
|
||||
* This will be auto-detected at runtime unless specifically set in the constructor.
|
||||
*
|
||||
* @var bool|null
|
||||
* @since 3.0.240
|
||||
*
|
||||
*/
|
||||
protected $usesNumericKeys = null;
|
||||
|
||||
/**
|
||||
* Construct
|
||||
*
|
||||
*/
|
||||
public function __construct() {
|
||||
if($this->className() === 'WireArray') $this->duplicateChecking = false;
|
||||
if($this->className() === 'WireArray') {
|
||||
$this->duplicateChecking = false;
|
||||
$this->indexedByName = false;
|
||||
$this->usesNumericKeys = true;
|
||||
}
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -296,7 +330,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
$key = key($this->data);
|
||||
}
|
||||
|
||||
$this->trackChange("add", null, $item);
|
||||
if($this->trackChanges) $this->trackChange("add", null, $item);
|
||||
$this->trackAdd($item, $key);
|
||||
|
||||
return $this;
|
||||
@@ -461,7 +495,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
throw new WireException("Key '$key' is not an allowed key for " . get_class($this));
|
||||
}
|
||||
|
||||
$this->trackChange($key, isset($this->data[$key]) ? $this->data[$key] : null, $value);
|
||||
if($this->trackChanges) $this->trackChange($key, isset($this->data[$key]) ? $this->data[$key] : null, $value);
|
||||
$this->data[$key] = $value;
|
||||
$this->trackAdd($value, $key);
|
||||
return $this;
|
||||
@@ -602,7 +636,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
if(isset($this->data[$k])) {
|
||||
$match = $this->data[$k];
|
||||
} else if($numericKeys) {
|
||||
$match = $this->getItemThatMatches('name', $k);
|
||||
$match = $this->getItemThatMatches($this->nameProperty, $k);
|
||||
}
|
||||
if($match) break;
|
||||
}
|
||||
@@ -613,7 +647,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
// if the WireArray uses numeric keys, then it's okay to
|
||||
// match a 'name' field if the provided key is a string
|
||||
if($this->usesNumericKeys()) {
|
||||
$match = $this->getItemThatMatches('name', $key);
|
||||
$match = $this->getItemThatMatches($this->nameProperty, $key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -760,7 +794,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
$match = $this->findOne($key);
|
||||
|
||||
} else if($this->usesNumericKeys()) {
|
||||
$match = $this->getItemThatMatches('name', $key);
|
||||
$match = $this->getItemThatMatches($this->nameProperty, $key);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -997,7 +1031,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
reset($this->data);
|
||||
$key = key($this->data);
|
||||
}
|
||||
$this->trackChange('prepend', null, $item);
|
||||
if($this->trackChanges) $this->trackChange('prepend', null, $item);
|
||||
$this->trackAdd($item, $key);
|
||||
return $this;
|
||||
}
|
||||
@@ -1058,7 +1092,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
$key = key($this->data);
|
||||
$item = array_shift($this->data);
|
||||
if(is_null($item)) return null;
|
||||
$this->trackChange('shift', $item, null);
|
||||
if($this->trackChanges) $this->trackChange('shift', $item, null);
|
||||
$this->trackRemove($item, $key);
|
||||
return $item;
|
||||
}
|
||||
@@ -1094,7 +1128,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
$key = key($this->data);
|
||||
$item = array_pop($this->data);
|
||||
if(is_null($item)) return null;
|
||||
$this->trackChange('pop', $item, null);
|
||||
if($this->trackChanges) $this->trackChange('pop', $item, null);
|
||||
$this->trackRemove($item, $key);
|
||||
return $item;
|
||||
}
|
||||
@@ -1119,7 +1153,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
$data[$key] = $this->data[$key];
|
||||
}
|
||||
|
||||
$this->trackChange('shuffle', $this->data, $data);
|
||||
if($this->trackChanges) $this->trackChange('shuffle', $this->data, $data);
|
||||
|
||||
$this->data = $data;
|
||||
|
||||
@@ -1219,7 +1253,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
if(array_key_exists($key, $this->data)) {
|
||||
$item = $this->data[$key];
|
||||
unset($this->data[$key]);
|
||||
$this->trackChange("remove", $item, null);
|
||||
if($this->trackChanges) $this->trackChange("remove", $item, null);
|
||||
$this->trackRemove($item, $key);
|
||||
} else if(!$obj && is_string($key) && Selectors::stringHasSelector($key)) {
|
||||
foreach($this->find($key) as $item) {
|
||||
@@ -1329,6 +1363,13 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
if(!$isArray) $properties = explode(',', $properties);
|
||||
|
||||
if(empty($properties)) return $this;
|
||||
|
||||
if($propertiesStr === $this->nameProperty && $this->indexedByName) {
|
||||
// optimization when it's a very simple sort by name
|
||||
ksort($this->data, $this->sortFlags);
|
||||
if($this->trackChanges) $this->trackChange("sort:$propertiesStr");
|
||||
return $this;
|
||||
}
|
||||
|
||||
// shortcut for random (only allowed as the sole sort property)
|
||||
// no warning/error for issuing more properties though
|
||||
@@ -1611,9 +1652,11 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
if(is_array($v)) $v = implode(' ', $this->wire()->sanitizer->flatArray($v));
|
||||
$value[] = (string) $v;
|
||||
}
|
||||
} else {
|
||||
} else if($item instanceof Wire) {
|
||||
$value = $this->getItemPropertyValue($item, $selector->field);
|
||||
$value = is_array($value) ? $this->wire()->sanitizer->flatArray($value) : (string) $value;
|
||||
} else {
|
||||
$value = $item; // integer, string, etc. (non-Wire object)
|
||||
}
|
||||
if($not === $selector->matches($value) && isset($this->data[$key])) {
|
||||
$qtyMatch++;
|
||||
@@ -1934,13 +1977,15 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
*
|
||||
*/
|
||||
public function __toString() {
|
||||
$s = '';
|
||||
foreach($this as $value) {
|
||||
$values = array();
|
||||
foreach($this->data as $value) {
|
||||
if(is_array($value)) $value = "array(" . count($value) . ")";
|
||||
$s .= "$value|";
|
||||
$value = (string) $value;
|
||||
if(!strlen($value)) continue;
|
||||
if(strpos($value, '|') !== false) $value = str_replace('|', ' ', $value);
|
||||
$values[] = $value;
|
||||
}
|
||||
$s = rtrim($s, '|');
|
||||
return $s;
|
||||
return implode('|', $values);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1998,7 +2043,20 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
*
|
||||
*/
|
||||
protected function trackAdd($item, $key) {
|
||||
if($key) {}
|
||||
if($key !== null && $key !== false) {
|
||||
if($this->usesNumericKeys === null) {
|
||||
$this->usesNumericKeys = is_int($key);
|
||||
}
|
||||
if($this->indexedByName === null) {
|
||||
$this->indexedByName = false;
|
||||
if($item instanceof WireData) {
|
||||
$name = $item->get($this->nameProperty);
|
||||
if($name === $key && isset($this->data[$name]) && $this->data[$name] === $item) {
|
||||
$this->indexedByName = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if($this->trackChanges()) $this->itemsAdded[] = $item;
|
||||
// wire this WireArray to the same instance of $item, if it isn’t already wired
|
||||
if($this->_wire === null && $item instanceof Wire && $item->isWired()) $item->wire($this);
|
||||
@@ -2116,16 +2174,29 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
*/
|
||||
protected function usesNumericKeys() {
|
||||
|
||||
static $testItem = null;
|
||||
static $usesNumericKeys = null;
|
||||
if($this->usesNumericKeys !== null) {
|
||||
return $this->usesNumericKeys;
|
||||
}
|
||||
|
||||
if(!empty($this->data)) {
|
||||
reset($this->data);
|
||||
$key = key($this->data);
|
||||
if($key !== null) {
|
||||
$this->usesNumericKeys = is_int($key);
|
||||
return $this->usesNumericKeys;
|
||||
}
|
||||
}
|
||||
|
||||
$testItem = $this->makeBlankItem();
|
||||
|
||||
if($testItem === null) {
|
||||
$this->usesNumericKeys = true;
|
||||
} else {
|
||||
$key = $this->getItemKey($testItem);
|
||||
$this->usesNumericKeys = is_int($key);
|
||||
}
|
||||
|
||||
if(!is_null($usesNumericKeys)) return $usesNumericKeys;
|
||||
if(is_null($testItem)) $testItem = $this->makeBlankItem();
|
||||
if(is_null($testItem)) return true;
|
||||
|
||||
$key = $this->getItemKey($testItem);
|
||||
$usesNumericKeys = is_int($key) ? true : false;
|
||||
return $usesNumericKeys;
|
||||
return $this->usesNumericKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2550,7 +2621,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
}
|
||||
} else {
|
||||
// array or string or null
|
||||
if(is_null($func)) $func = 'name';
|
||||
if(is_null($func)) $func = $this->nameProperty;
|
||||
$result = $this->explode($func);
|
||||
}
|
||||
|
||||
@@ -2656,7 +2727,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
|
||||
$item = $item->debugInfoSmall();
|
||||
} else if($item instanceof WireData) {
|
||||
$_item = $item;
|
||||
$item = $item->get('name');
|
||||
$item = $item->get($this->nameProperty);
|
||||
if(!$item) $item = $_item->get('id');
|
||||
if(!$item) $item = $_item->className();
|
||||
} else {
|
||||
|
@@ -602,8 +602,10 @@ class WireCache extends Wire {
|
||||
*
|
||||
*/
|
||||
public function getExpires($expire, $verbose = null) {
|
||||
|
||||
if($expire instanceof Wire && $expire->id) {
|
||||
|
||||
$isString = is_string($expire);
|
||||
|
||||
if(!$isString && $expire instanceof Wire && $expire->id) {
|
||||
|
||||
if($expire instanceof Page) {
|
||||
// page object
|
||||
@@ -618,7 +620,7 @@ class WireCache extends Wire {
|
||||
$expire = time() + self::expireDaily;
|
||||
}
|
||||
|
||||
} else if(is_array($expire)) {
|
||||
} else if(!$isString && is_array($expire)) {
|
||||
// expire value already prepared by a previous call, just return it
|
||||
if(isset($expire['selector']) && isset($expire['expire'])) {
|
||||
if($verbose || $verbose === null) return $expire; // return array
|
||||
@@ -628,11 +630,11 @@ class WireCache extends Wire {
|
||||
$expire = self::expireDaily;
|
||||
}
|
||||
|
||||
} else if(is_string($expire) && isset($this->expireNames[$expire])) {
|
||||
} else if($isString && isset($this->expireNames[$expire])) {
|
||||
// named expiration constant like "hourly", "daily", etc.
|
||||
$expire = time() + $this->expireNames[$expire];
|
||||
|
||||
} else if(is_string($expire) && Selectors::stringHasSelector($expire)) {
|
||||
} else if($isString && Selectors::stringHasSelector($expire)) {
|
||||
// expire when page matches selector
|
||||
if($verbose || $verbose === null) {
|
||||
return array(
|
||||
@@ -649,7 +651,7 @@ class WireCache extends Wire {
|
||||
} else {
|
||||
|
||||
// account for date format as string
|
||||
if(is_string($expire) && !ctype_digit("$expire")) {
|
||||
if($isString && !ctype_digit("$expire")) {
|
||||
$expire = strtotime($expire);
|
||||
$isDate = true;
|
||||
} else {
|
||||
@@ -870,7 +872,7 @@ class WireCache extends Wire {
|
||||
if(!$forceRun) {
|
||||
// run general maintenance only once every 10 minutes
|
||||
$filename = $this->wire()->config->paths->cache . 'WireCache.maint';
|
||||
if(@filemtime($filename) > (time() - 600)) return false;
|
||||
if(file_exists($filename) && filemtime($filename) > (time() - 600)) return false;
|
||||
touch($filename);
|
||||
}
|
||||
|
||||
|
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* Database cache handler for WireCache
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @since 2.0.218
|
||||
@@ -205,6 +205,38 @@ class WireCacheDatabase extends Wire implements WireCacheInterface {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Database cache maintenance (every 10 minutes)
|
||||
*
|
||||
* @param Template|Page $obj
|
||||
* @return bool
|
||||
* @throws WireException
|
||||
* @since 3.0.242
|
||||
*
|
||||
*/
|
||||
public function maintenance($obj) {
|
||||
|
||||
if($obj) return false; // let WireCache handle when object value is provided
|
||||
|
||||
$sql =
|
||||
'DELETE FROM caches ' .
|
||||
'WHERE (expires<=:now AND expires>:never) ' .
|
||||
'OR expires<:then';
|
||||
|
||||
$query = $this->wire()->database->prepare($sql);
|
||||
$query->bindValue(':now', date(WireCache::dateFormat, time()));
|
||||
$query->bindValue(':never', WireCache::expireNever);
|
||||
$query->bindValue(':then', '1974-10-10 10:10:10');
|
||||
$query->execute();
|
||||
$qty = $query->rowCount();
|
||||
|
||||
if($qty) $this->wire->cache->log(
|
||||
sprintf('DB cache maintenance expired %d cache(s)', $qty)
|
||||
);
|
||||
|
||||
return $qty > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the caches table if it happens to have been deleted
|
||||
*
|
||||
|
@@ -6,7 +6,7 @@
|
||||
* A WireData object that maintains its data in a database table rather than just in memory.
|
||||
* An example of usage is the `$page->meta()` method.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2019
|
||||
* ProcessWire 3.x, Copyright 2023
|
||||
* https://processwire.com
|
||||
*
|
||||
*/
|
||||
@@ -215,7 +215,7 @@ class WireDataDB extends WireData implements \Countable {
|
||||
$sql =
|
||||
"INSERT INTO `$table` (source_id, name, data) VALUES(:source_id, :name, :data) " .
|
||||
"ON DUPLICATE KEY UPDATE source_id=VALUES(source_id), name=VALUES(name), data=VALUES(data)";
|
||||
$query = $this->wire('database')->prepare($sql);
|
||||
$query = $this->wire()->database->prepare($sql);
|
||||
$query->bindValue(':source_id', $this->sourceID(), \PDO::PARAM_INT);
|
||||
$query->bindValue(':name', $name);
|
||||
$query->bindValue(':data', $data);
|
||||
|
@@ -49,7 +49,7 @@
|
||||
* ~~~~~
|
||||
* #pw-body
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*
|
||||
@@ -834,10 +834,14 @@ class WireDatabaseBackup {
|
||||
if(in_array($table, $options['excludeExportTables'])) continue;
|
||||
$numTables++;
|
||||
$columns = array();
|
||||
$columnTypes = array();
|
||||
$query = $database->prepare("SHOW COLUMNS FROM `$table`");
|
||||
$query->execute();
|
||||
/** @noinspection PhpAssignmentInConditionInspection */
|
||||
while($row = $query->fetch(\PDO::FETCH_NUM)) $columns[] = $row[0];
|
||||
while($row = $query->fetch(\PDO::FETCH_NUM)) {
|
||||
$columns[] = $row[0];
|
||||
$columnTypes[] = $row[1];
|
||||
}
|
||||
$query->closeCursor();
|
||||
$columnsStr = '`' . implode('`, `', $columns) . '`';
|
||||
|
||||
@@ -862,9 +866,12 @@ class WireDatabaseBackup {
|
||||
while($row = $query->fetch(\PDO::FETCH_NUM)) {
|
||||
$numInserts++;
|
||||
$out = "\nINSERT INTO `$table` ($columnsStr) VALUES(";
|
||||
foreach($row as $value) {
|
||||
foreach($row as $key => $value) {
|
||||
$columnType = $columnTypes[$key];
|
||||
if(is_null($value)) {
|
||||
$value = 'NULL';
|
||||
} else if(stripos($columnType, 'bit') === 0 && ctype_digit("$value")) {
|
||||
// leave bit column value unquoted
|
||||
} else {
|
||||
if($hasReplace) foreach($options['findReplace'] as $find => $replace) {
|
||||
if(strpos($value, $find)) $value = str_replace($find, $replace, $value);
|
||||
|
@@ -475,14 +475,58 @@ class WireDatabasePDO extends Wire implements WireDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the current PDO connection(s)
|
||||
*
|
||||
* This forces re-creation of the PDO instance(s), whether writer, reader or both.
|
||||
* This may be useful to call after a "MySQL server has gone away" error to attempt
|
||||
* to re-establish the connection.
|
||||
*
|
||||
* #pw-group-connection
|
||||
*
|
||||
* @param string|null $type
|
||||
* - Specify 'writer' to reset writer instance.
|
||||
* - Specify 'reader' to reset reader instance.
|
||||
* - Omit or null to reset both, or whichever one is in use.
|
||||
* @return self
|
||||
* @since 3.0.240
|
||||
*
|
||||
*/
|
||||
public function reset($type = null) {
|
||||
$this->close($type);
|
||||
$this->pdo($type);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the current PDO connection(s)
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string|null $type
|
||||
* - Specify 'writer' to close writer instance.
|
||||
* - Specify 'reader' to close reader instance.
|
||||
* - Omit or null to close both.
|
||||
* @return self
|
||||
* @since 3.0.240
|
||||
*
|
||||
*/
|
||||
public function close($type = null) {
|
||||
if($type === 'reader' || $type === null) {
|
||||
$this->reader['pdo'] = null;
|
||||
}
|
||||
if($type === 'writer' || $type === null) {
|
||||
$this->writer['pdo'] = null;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual current PDO connection instance
|
||||
*
|
||||
* If connection is lost, this will restore it automatically.
|
||||
* #pw-internal
|
||||
*
|
||||
* #pw-group-connection
|
||||
*
|
||||
* @param string|\PDOStatement|null SQL, statement, or statement type (reader or primary) (3.0.175+)
|
||||
* @param string|\PDOStatement|null SQL, statement, or statement type (reader or writer) (3.0.175+)
|
||||
*
|
||||
* @return \PDO
|
||||
*
|
||||
@@ -593,6 +637,14 @@ class WireDatabasePDO extends Wire implements WireDatabase {
|
||||
} else if(stripos($statement, 'select') === 0) {
|
||||
// select query is always reader
|
||||
$type = $reader;
|
||||
// check that this is not an InnoDB 'SELECT' '… FOR UPDATE' or '… FOR SHARE' query
|
||||
$forpos = $this->engine === 'innodb' ? strripos($query, 'for') : 0;
|
||||
if($forpos) {
|
||||
$for = ltrim(strtolower(substr($query, $forpos+4, 15)));
|
||||
if(stripos($for, 'update') === 0 || stripos($for, 'share') === 0) {
|
||||
$type = $writer;
|
||||
}
|
||||
}
|
||||
} else if(stripos($statement, 'insert') === 0) {
|
||||
// insert query is always writer
|
||||
$type = $writer;
|
||||
@@ -810,6 +862,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
|
||||
*
|
||||
*/
|
||||
public function commit() {
|
||||
if(!$this->inTransaction()) return false;
|
||||
$this->allowReader(true);
|
||||
return $this->pdoWriter()->commit();
|
||||
}
|
||||
@@ -824,6 +877,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
|
||||
*
|
||||
*/
|
||||
public function rollBack() {
|
||||
if(!$this->inTransaction()) return false;
|
||||
$this->allowReader(true);
|
||||
return $this->pdoWriter()->rollBack();
|
||||
}
|
||||
@@ -925,31 +979,41 @@ class WireDatabasePDO extends Wire implements WireDatabase {
|
||||
*
|
||||
* @param \PDOStatement $query
|
||||
* @param bool $throw Whether or not to throw exception on query error (default=true)
|
||||
* @param int $maxTries Deprecated/argument does nothing (was: “Max number of times it will attempt to retry query on error”)
|
||||
* @param int $maxTries Max number of times it will attempt to retry query on lost connection error
|
||||
* @return bool True on success, false on failure. Note if you want this, specify $throw=false in your arguments.
|
||||
* @throws \PDOException
|
||||
*
|
||||
*/
|
||||
public function execute(\PDOStatement $query, $throw = true, $maxTries = 3) {
|
||||
$tries = 0;
|
||||
|
||||
try {
|
||||
$result = $query->execute();
|
||||
} catch(\PDOException $e) {
|
||||
$result = false;
|
||||
if($query->errorCode() == '42S22') {
|
||||
// unknown column error
|
||||
$errorInfo = $query->errorInfo();
|
||||
if(preg_match('/[\'"]([_a-z0-9]+\.[_a-z0-9]+)[\'"]/i', $errorInfo[2], $matches)) {
|
||||
$this->unknownColumnError($matches[1]);
|
||||
do {
|
||||
$tryAgain = false;
|
||||
try {
|
||||
$result = $query->execute();
|
||||
} catch(\PDOException $e) {
|
||||
$result = false;
|
||||
if($query->errorCode() == '42S22') {
|
||||
// unknown column error
|
||||
$errorInfo = $query->errorInfo();
|
||||
if(preg_match('/[\'"]([_a-z0-9]+\.[_a-z0-9]+)[\'"]/i', $errorInfo[2], $matches)) {
|
||||
$this->unknownColumnError($matches[1]);
|
||||
}
|
||||
} else if($e->getCode() === 'HY000' && $tries < $maxTries) {
|
||||
// mysql server has gone away
|
||||
$this->reset();
|
||||
$tryAgain = true;
|
||||
$tries++;
|
||||
}
|
||||
if($tryAgain) {
|
||||
// we will try again on next iteration
|
||||
} else if($throw) {
|
||||
throw $e;
|
||||
} else {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
if($throw) {
|
||||
throw $e;
|
||||
} else {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
if($maxTries) {} // ignore, argument no longer used
|
||||
}
|
||||
} while($tryAgain);
|
||||
|
||||
return $result;
|
||||
}
|
||||
@@ -1907,4 +1971,3 @@ class WireDatabasePDO extends Wire implements WireDatabase {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -421,7 +421,7 @@ class WireDateTime extends Wire {
|
||||
$value = $this->relativeTimeStr($ts, 1, false);
|
||||
} else if($format == 'ts') {
|
||||
$value = $ts;
|
||||
} else if(strpos($format, '%') !== false && version_compare(PHP_VERSION, '8.1.0', '<')) {
|
||||
} else if(strpos($format, '%') !== false) {
|
||||
$value = $this->strftime($format, $ts);
|
||||
} else {
|
||||
$value = date($format, $ts);
|
||||
@@ -435,13 +435,16 @@ class WireDateTime extends Wire {
|
||||
* Parse about any English textual datetime description into a Unix timestamp using PHP’s strtotime()
|
||||
*
|
||||
* This function behaves the same as PHP’s version except that it optionally accepts an `$options` array
|
||||
* and lets you specify the return value for empty or zeroed dates like 0000-00-00. If given a zerod date
|
||||
* then it returns null by default (rather than throwing an error as PHP8 does).
|
||||
* and lets you specify the return value for empty or zeroed dates like 0000-00-00. If given a zero’d date
|
||||
* then it returns null by default (rather than throwing an error as PHP8 does). As of 3.0.238+ this method
|
||||
* also lets you optionally specify an input format should the given date string not be strtotime compatible.
|
||||
*
|
||||
* @param string $str Date/time string
|
||||
* @param array|int $options Options to modify behavior, or specify int for the `baseTimestamp` option.
|
||||
* @param array|int $options Options to modify behavior, or specify int for the `baseTimestamp` option, or string for `inputFormat` option.
|
||||
* - `emptyReturnValue` (int|null|false): Value to return for empty or zero-only date strings (default=null)
|
||||
* - `baseTimestamp` (int|null): The timestamp which is used as a base for the calculation of relative dates.
|
||||
* - `inputFormat` (string): Optional date format that given $str is in, if not strtotime() compatible. (3.0.238+)
|
||||
* - `outputFormat` (string): Optionally return value in this date format rather than unix timestamp (3.0.238+)
|
||||
* @return false|int|null
|
||||
* @see https://www.php.net/manual/en/function.strtotime.php
|
||||
* @since 3.0.178
|
||||
@@ -451,17 +454,110 @@ class WireDateTime extends Wire {
|
||||
$defaults = array(
|
||||
'emptyReturnValue' => null,
|
||||
'baseTimestamp' => null,
|
||||
'inputFormat' => '',
|
||||
'outputFormat' => '',
|
||||
);
|
||||
if(is_int($options)) $defaults['baseTimestamp'] = $options;
|
||||
$options = is_array($options) ? array_merge($defaults, $options) : $defaults;
|
||||
if(!empty($options['outputFormat'])) return $this->strtodate($str, $options);
|
||||
$str = trim($str);
|
||||
if(empty($str)) return $options['emptyReturnValue'];
|
||||
if(strpos($str, '00') === 0) {
|
||||
$test = trim(preg_replace('/[^\d]/', '', $str), '0');
|
||||
if(!strlen($test)) return $options['emptyReturnValue'];
|
||||
}
|
||||
if($options['baseTimestamp'] === null) return strtotime($str);
|
||||
return strtotime($str, $options['baseTimestamp']) ;
|
||||
if($options['inputFormat']) {
|
||||
$value = \DateTimeImmutable::createFromFormat($options['inputFormat'], $str);
|
||||
$value = $value ? $value->getTimestamp() : false;
|
||||
} else {
|
||||
$value = strtotime($str, $options['baseTimestamp']) ;
|
||||
}
|
||||
if($value === false) $value = $options['emptyReturnValue'];
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse English textual datetime description into a formatted date string, or blank if not a date
|
||||
*
|
||||
* @param string $str Date/time string to parse
|
||||
* @param string|array $format Output format to use, or array for $options.
|
||||
* - Omit or boolean true for default 'Y-m-d H:i:s'.
|
||||
* - Specify date format string, see [formats](https://www.php.net/manual/en/datetime.format.php).
|
||||
* - Specify boolean false for unix timestamp.
|
||||
* - Specify array of options.
|
||||
* @param array $options Can also be specified as 2nd argument. Options include:
|
||||
* - `emptyReturnValue` (int|null|false): Value to return for empty or zero-only date strings (default='')
|
||||
* - `baseTimestamp` (int|null): The timestamp which is used as a base for the calculation of relative dates.
|
||||
* - `inputFormat` (string): Optional date format that given $str is in, if not strtotime() compatible.
|
||||
* - `outputFormat` (string|bool): Format to return date string in, used only if $options specified for $format argument.
|
||||
* - `format` (string|bool) Optional alias of outputFormat, used only if $options specified for $format argument.
|
||||
* @return string Return string, returns blank string on fail.
|
||||
* @since 3.0.238
|
||||
*
|
||||
*/
|
||||
public function strtodate($str, $format = true, array $options = array()) {
|
||||
$defaults = array(
|
||||
'emptyReturnValue' => '',
|
||||
'baseTimestamp' => null,
|
||||
'outputFormat' => 'Y-m-d H:i:s',
|
||||
'inputFormat' => '',
|
||||
);
|
||||
|
||||
if(is_array($format)) {
|
||||
$options = array_merge($defaults, $format);
|
||||
if(isset($options['format'])) $options['outputFormat'] = $options['format'];
|
||||
$format = $options['outputFormat'];
|
||||
} else {
|
||||
$options = array_merge($defaults, $options);
|
||||
}
|
||||
|
||||
$is = false;
|
||||
$str = trim((string) $str);
|
||||
$len = strlen($str);
|
||||
|
||||
if($format === true) $format = $defaults['outputFormat'];
|
||||
|
||||
if(!$len || $len > 30) {
|
||||
return $options['emptyReturnValue'];
|
||||
|
||||
} else if(ctype_digit($str) && strpos($str, '0') !== 0) {
|
||||
if($len === 1) $str = "0$str";
|
||||
if($len === 2) {
|
||||
$value = strtotime("20$str-01-01");
|
||||
} else if($len === 4) {
|
||||
$value = strtotime("$str-01-01");
|
||||
} else {
|
||||
// unix timestamp
|
||||
$value = (int) $str;
|
||||
if(is_int($options['baseTimestamp'])) $value += $options['baseTimestamp'];
|
||||
}
|
||||
|
||||
} else {
|
||||
// i.e. '+1 DAY', '10/10/2024', 'April 8 2024'
|
||||
$chars = array('-', '/', '+', '.', ' ');
|
||||
foreach($chars as $c) {
|
||||
if(strpos($str, $c) !== false) $is = true;
|
||||
if($is) break;
|
||||
}
|
||||
if(!$is) $is = ctype_alnum($str); // word string with 0 space, i.e. "tomorrow"
|
||||
if(!$is) return $options['emptyReturnValue'];
|
||||
unset($options['outputFormat']);
|
||||
$value = $this->strtotime($str, $options);
|
||||
}
|
||||
|
||||
if($value !== $options['emptyReturnValue']) {
|
||||
if($format === $defaults['outputFormat']) {
|
||||
$value = date($format, $value);
|
||||
} else if(empty($format) || $format === 'ts' || $format === 'U') {
|
||||
// timestamp, keep as-is
|
||||
} else {
|
||||
$value = $this->date($format, $value);
|
||||
}
|
||||
}
|
||||
|
||||
if(empty($value)) $value = $options['emptyReturnValue'];
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1103,4 +1199,7 @@ class WireDateTime extends Wire {
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
public function isStrtotime($str) {
|
||||
}
|
||||
}
|
||||
|
@@ -1676,25 +1676,31 @@ class WireFileTools extends Wire {
|
||||
// note: this fails for PHP files executable on their own (like shell scripts)
|
||||
return $namespace;
|
||||
}
|
||||
|
||||
// get everything that appears before "namespace" keyword
|
||||
$head = substr($data, 0, $namespacePos);
|
||||
|
||||
// find where line ends after "namespace ..." keyword
|
||||
foreach(array("\n", "\r", ";") as $c) {
|
||||
$eol = strpos($data, $c, $namespacePos);
|
||||
if($eol !== false) break;
|
||||
}
|
||||
|
||||
// get everything that appears before "namespace", and after "namespace" on same line
|
||||
$head = $eol === false ? $data : substr($data, 0, $eol);
|
||||
$headPrev = $head;
|
||||
|
||||
// declare(...); is the one statement allowed to appear before namespace in PHP files
|
||||
if(strpos($head, 'declare')) {
|
||||
$head = preg_replace('/declare[ ]*\(.+?\)[ ]*;\s*/s', '', $head);
|
||||
// single line comment(s) appear before namespace
|
||||
if(strpos($head, '//') !== false) {
|
||||
$head = preg_replace('!//[^\r\n]*!', '', $head);
|
||||
}
|
||||
|
||||
// single line comment(s) appear before namespace
|
||||
if(strpos($head, '//') !== false) {
|
||||
$head = preg_replace('!//.*!', '', $head);
|
||||
}
|
||||
|
||||
// single or multi-line comments before namespace
|
||||
if(strpos($head, '/' . '*') !== false) {
|
||||
$head = preg_replace('!/\*.*\*/!s', '', $head);
|
||||
}
|
||||
|
||||
// declare(...); is the one statement allowed to appear before namespace in PHP files
|
||||
if(strpos($head, 'declare')) {
|
||||
$head = preg_replace('/declare[ ]*\(.+?\)[ ]*;\s*/s', '', $head);
|
||||
}
|
||||
|
||||
// replace cleaned up head in data
|
||||
if($head !== $headPrev) {
|
||||
|
@@ -47,6 +47,7 @@ class WireHooks {
|
||||
* - fromClass: the name of the class containing the hooked method, if not the object where addHook was executed. Set automatically, but you may still use in some instances.
|
||||
* - argMatch: array of Selectors objects where the indexed argument (n) to the hooked method must match, order to execute hook.
|
||||
* - objMatch: Selectors object that the current object must match in order to execute hook
|
||||
* - retMatch: Selectors object that must match the return value, or a match string to match return value
|
||||
* - public: auto-assigned to true or false by addHook() as to whether the method is public or private/protected.
|
||||
*
|
||||
*/
|
||||
@@ -58,7 +59,10 @@ class WireHooks {
|
||||
'allInstances' => false,
|
||||
'fromClass' => '',
|
||||
'argMatch' => null,
|
||||
'argMatchType' => [],
|
||||
'objMatch' => null,
|
||||
'retMatch' => null,
|
||||
'retMatchType' => '',
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -334,7 +338,7 @@ class WireHooks {
|
||||
* @see WireHooks::isMethodHooked(), WireHooks::isPropertyHooked(), WireHooks::hasHook()
|
||||
*
|
||||
*/
|
||||
public function isHooked($method, Wire $instance = null) {
|
||||
public function isHooked($method, ?Wire $instance = null) {
|
||||
if($instance) return $this->hasHook($instance, $method);
|
||||
if(strpos($method, ':') !== false) {
|
||||
$hooked = isset($this->hookClassMethodCache[$method]); // fromClass::method() or fromClass::property
|
||||
@@ -631,8 +635,21 @@ class WireHooks {
|
||||
$options['fromClass'] = $fromClass;
|
||||
}
|
||||
|
||||
$retMatch = '';
|
||||
$argOpen = strpos($method, '(');
|
||||
if($argOpen) {
|
||||
|
||||
if($argOpen) {
|
||||
if(strpos($method, ':(')) {
|
||||
list($method, $retMatch) = explode(':(', $method, 2);
|
||||
$retMatch = rtrim($retMatch, ') ');
|
||||
} else if(strpos($method, ':<') && substr(trim($method), -1) === '>') {
|
||||
list($method, $retMatch) = explode(':<', $method, 2);
|
||||
$retMatch = "<$retMatch";
|
||||
}
|
||||
$argOpen = strpos($method, '(');
|
||||
}
|
||||
|
||||
if($argOpen) {
|
||||
// arguments to match may be specified in method name
|
||||
$argClose = strpos($method, ')');
|
||||
if($argClose === $argOpen+1) {
|
||||
@@ -659,18 +676,31 @@ class WireHooks {
|
||||
// just single argument specified, so argument 0 is assumed
|
||||
}
|
||||
if(is_string($argMatch)) $argMatch = array(0 => $argMatch);
|
||||
$argMatchType = [];
|
||||
foreach($argMatch as $argKey => $argVal) {
|
||||
if(Selectors::stringHasSelector($argVal)) {
|
||||
/** @var Selectors $selectors */
|
||||
$selectors = $this->wire->wire(new Selectors());
|
||||
$selectors->init($argVal);
|
||||
$argMatch[$argKey] = $selectors;
|
||||
}
|
||||
list($argVal, $argValType) = $this->prepareArgMatch($argVal);
|
||||
$argMatch[$argKey] = $argVal;
|
||||
$argMatchType[$argKey] = $argValType;
|
||||
}
|
||||
if(count($argMatch)) {
|
||||
$options['argMatch'] = $argMatch;
|
||||
$options['argMatchType'] = $argMatchType;
|
||||
}
|
||||
if(count($argMatch)) $options['argMatch'] = $argMatch;
|
||||
}
|
||||
} else if(strpos($method, ':')) {
|
||||
list($method, $retMatch) = explode(':', $method, 2);
|
||||
}
|
||||
|
||||
if($retMatch) {
|
||||
// match return value
|
||||
if($options['before'] && !$options['after']) {
|
||||
throw new WireException('You cannot match return values with “before” hooks');
|
||||
}
|
||||
list($retMatch, $retMatchType) = $this->prepareArgMatch($retMatch);
|
||||
$options['retMatch'] = $retMatch;
|
||||
$options['retMatchType'] = $retMatchType;
|
||||
}
|
||||
|
||||
$localHooks = $object->getLocalHooks();
|
||||
|
||||
if($options['allInstances'] || $options['fromClass']) {
|
||||
@@ -996,50 +1026,25 @@ class WireHooks {
|
||||
if($type == 'method' && !empty($hook['options']['argMatch'])) {
|
||||
// argument comparison to determine at runtime whether to execute the hook
|
||||
$argMatches = $hook['options']['argMatch'];
|
||||
$argMatchTypes = $hook['options']['argMatchType'];
|
||||
$matches = true;
|
||||
foreach($argMatches as $argKey => $argMatch) {
|
||||
/** @var Selectors $argMatch */
|
||||
$argMatchType = isset($argMatchTypes[$argKey]) ? $argMatchTypes[$argKey] : '';
|
||||
$argVal = isset($arguments[$argKey]) ? $arguments[$argKey] : null;
|
||||
if(is_object($argMatch)) {
|
||||
// Selectors object
|
||||
if(is_object($argVal)) {
|
||||
$matches = $argMatch->matches($argVal);
|
||||
} else {
|
||||
// we don't work with non-object here
|
||||
$matches = false;
|
||||
}
|
||||
} else if(is_string($argMatch) && strpos($argMatch, '<') === 0 && substr($argMatch, -1) === '>') {
|
||||
// i.e. <Page>, <User>, <string>, <object>, <bool>, etc.
|
||||
$argMatch = trim($argMatch, '<>');
|
||||
if(strpos($argMatch, '|')) {
|
||||
// i.e. <User|Role|Permission> or <int|float> etc.
|
||||
$argMatches = explode('|', str_replace(array('<', '>'), '', $argMatch));
|
||||
} else {
|
||||
$argMatches = array($argMatch);
|
||||
}
|
||||
foreach($argMatches as $argMatchType) {
|
||||
if(isset($this->argMatchTypes[$argMatchType])) {
|
||||
$argMatchFunc = $this->argMatchTypes[$argMatchType];
|
||||
$matches = $argMatchFunc($argVal);
|
||||
} else {
|
||||
$matches = wireInstanceOf($argVal, $argMatchType);
|
||||
}
|
||||
if($matches) break;
|
||||
}
|
||||
} else {
|
||||
if(is_array($argVal)) {
|
||||
// match any array element
|
||||
$matches = in_array($argMatch, $argVal);
|
||||
} else {
|
||||
// exact string match
|
||||
$matches = $argMatch == $argVal;
|
||||
}
|
||||
}
|
||||
$matches = $this->conditionalArgMatch($argMatch, $argVal, $argMatchType);
|
||||
if(!$matches) break;
|
||||
}
|
||||
if(!$matches) continue; // don't run hook
|
||||
}
|
||||
|
||||
if($type === 'method' && $when === 'after' && !empty($hook['options']['retMatch'])) {
|
||||
if(!$this->conditionalArgMatch(
|
||||
$hook['options']['retMatch'],
|
||||
$result['return'],
|
||||
$hook['options']['retMatchType'])) continue;
|
||||
}
|
||||
|
||||
if($this->allowPathHooks && isset($this->pathHooks[$hook['id']])) {
|
||||
$allowRunPathHook = $this->allowRunPathHook($hook['id'], $arguments);
|
||||
$this->removeHook($object, $hook['id']); // once only
|
||||
@@ -1135,9 +1140,104 @@ class WireHooks {
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow given path hook to run?
|
||||
* Prepare argument match
|
||||
*
|
||||
* This checks if the hook’s path matches the request path, allowing for both
|
||||
* @param string $argMatch
|
||||
* @return array
|
||||
* @since 3.0.247
|
||||
*
|
||||
*/
|
||||
protected function prepareArgMatch($argMatch) {
|
||||
$argMatch = trim($argMatch, '()');
|
||||
$argMatchType = '';
|
||||
|
||||
list($c1, $c2, $c3) = [ substr($argMatch, 0, 1), substr($argMatch, -1), substr($argMatch, 0, 2) ];
|
||||
|
||||
if($c1 === '<' && $c2 === '>') {
|
||||
// i.e. <WireArray> or <ThisPage|ThatPage>
|
||||
$argMatchType = 'instanceof';
|
||||
$argMatch = trim($argMatch, '<>');
|
||||
|
||||
} else if($c1 === '=' || $c1 === '<' || $c1 === '>' || Selectors::isOperator($c3)) {
|
||||
// selector that starts with operator and translates to "argVal matches argMatch"
|
||||
$argMatch = "___val$argMatch"; // i.e. ___val=something
|
||||
$argMatchType = 'selector';
|
||||
}
|
||||
|
||||
if($argMatchType === 'instanceof') {
|
||||
// ok
|
||||
$argMatch = strpos($argMatch, '|') ? explode('|', $argMatch) : [ $argMatch ];
|
||||
} else if(Selectors::stringHasSelector($argMatch)) {
|
||||
/** @var Selectors $selectors */
|
||||
$selectors = $this->wire->wire(new Selectors());
|
||||
$selectors->init($argMatch);
|
||||
$argMatch = $selectors;
|
||||
$argMatchType = 'selector';
|
||||
} else {
|
||||
$argMatchType = 'equals';
|
||||
}
|
||||
|
||||
return [ $argMatch, $argMatchType ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Does given value match given match condition?
|
||||
*
|
||||
* @param Selectors|string $argMatch
|
||||
* @param mixed $argVal
|
||||
* @return bool
|
||||
* @since 3.0.247
|
||||
*
|
||||
*/
|
||||
protected function conditionalArgMatch($argMatch, $argVal, $argMatchType) {
|
||||
|
||||
$matches = false;
|
||||
|
||||
if($argMatch instanceof Selectors) {
|
||||
// Selectors object
|
||||
/** @var Selector $s */
|
||||
$s = $argMatch->first();
|
||||
if($s instanceof Selector && $s->field() === '___val') {
|
||||
$o = WireData();
|
||||
$o->set('value', $argVal);
|
||||
$s->field = 'value';
|
||||
$argVal = $o;
|
||||
} else if(is_array($argVal)) {
|
||||
$argVal = count($argVal) && is_string(key($argVal)) ? WireData($argVal) : WireArray($argVal);
|
||||
}
|
||||
if(is_object($argVal)) {
|
||||
$matches = $argMatch->matches($argVal);
|
||||
}
|
||||
|
||||
} else if($argMatchType === 'instanceof') {
|
||||
if(!is_array($argMatch)) $argMatch = [ $argMatch ];
|
||||
foreach($argMatch as $type) {
|
||||
if(isset($this->argMatchTypes[$type])) {
|
||||
$argMatchFunc = $this->argMatchTypes[$type];
|
||||
$matches = $argMatchFunc($argVal);
|
||||
} else {
|
||||
$matches = wireInstanceOf($argVal, $type);
|
||||
}
|
||||
if($matches) break;
|
||||
}
|
||||
|
||||
} else if(is_array($argVal)) {
|
||||
// match any array element
|
||||
$matches = in_array($argMatch, $argVal);
|
||||
|
||||
} else {
|
||||
// exact match
|
||||
$matches = $argMatch == $argVal;
|
||||
}
|
||||
|
||||
return $matches;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow given path hook to run?
|
||||
*
|
||||
* This checks if the hook’s path matches the request path, allowing for both
|
||||
* regular and regex matches and populating parenthesized portions to arguments
|
||||
* that will appear in the HookEvent.
|
||||
*
|
||||
@@ -1175,6 +1275,15 @@ class WireHooks {
|
||||
$regexDelim = $matchPath[0];
|
||||
} else {
|
||||
// needs to be in regex format
|
||||
if(strpos($matchPath, '.') !== false) {
|
||||
// preserve some regex sequences containing periods
|
||||
$r = [ '.+' => '•+', '.*' => '•*', '\\.' => '\\•' ];
|
||||
$matchPath = str_replace(array_keys($r), array_values($r), $matchPath);
|
||||
// force any remaining periods to be taken literally
|
||||
$matchPath = str_replace('.', '\\.', $matchPath);
|
||||
// restore regex sequences containing periods
|
||||
$matchPath = str_replace(array_values($r), array_keys($r), $matchPath);
|
||||
}
|
||||
if(strpos($matchPath, '/') === 0) $matchPath = "^$matchPath";
|
||||
$matchPath = "#$matchPath$#";
|
||||
}
|
||||
|
@@ -76,7 +76,17 @@ class WireHttp extends Wire {
|
||||
* HTTP methods we are allowed to use
|
||||
*
|
||||
*/
|
||||
protected $allowHttpMethods = array('GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH');
|
||||
protected $allowHttpMethods = array(
|
||||
'GET',
|
||||
'POST',
|
||||
'PUT',
|
||||
'DELETE',
|
||||
'HEAD',
|
||||
'PATCH',
|
||||
'OPTIONS',
|
||||
'TRACE',
|
||||
'CONNECT'
|
||||
);
|
||||
|
||||
/**
|
||||
* Headers to include in the request
|
||||
@@ -2089,6 +2099,8 @@ class WireHttp extends Wire {
|
||||
/**
|
||||
* Set the number of seconds till connection times out
|
||||
*
|
||||
* Note that the default timeout for http requests is 4.5 seconds
|
||||
*
|
||||
* #pw-group-settings
|
||||
*
|
||||
* @param int|float $seconds
|
||||
|
@@ -3,9 +3,9 @@
|
||||
/**
|
||||
* ProcessWire Markup Regions
|
||||
*
|
||||
* Supportings finding and manipulating of markup regions in an HTML document.
|
||||
* Supports finding and manipulating of markup regions in an HTML document.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2025 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*/
|
||||
@@ -37,23 +37,40 @@ class WireMarkupRegions extends Wire {
|
||||
*
|
||||
*/
|
||||
protected $selfClosingTags = array(
|
||||
'link',
|
||||
'area',
|
||||
'base',
|
||||
'br',
|
||||
'col',
|
||||
'command',
|
||||
'embed',
|
||||
'hr',
|
||||
'img',
|
||||
'input',
|
||||
'keygen',
|
||||
'link',
|
||||
'meta',
|
||||
'param',
|
||||
'source',
|
||||
'track',
|
||||
'wbr',
|
||||
'link' => 'link',
|
||||
'area' => 'area',
|
||||
'base' => 'base',
|
||||
'br' => 'br',
|
||||
'col' => 'col',
|
||||
'command' => 'command',
|
||||
'embed' => 'embed',
|
||||
'hr' => 'hr',
|
||||
'img' => 'img',
|
||||
'input' => 'input',
|
||||
'keygen' => 'keygen',
|
||||
'link' => 'link',
|
||||
'meta' => 'meta',
|
||||
'param' => 'param',
|
||||
'source' => 'source',
|
||||
'track' => 'track',
|
||||
'wbr' => 'wbr',
|
||||
);
|
||||
|
||||
/**
|
||||
* Tags that generally only appear once in the output
|
||||
*
|
||||
* These can be used as unnamed markup regions
|
||||
*
|
||||
* @var string[]
|
||||
*
|
||||
*/
|
||||
protected $singles = array(
|
||||
'html' => 'html',
|
||||
'head' => 'head',
|
||||
'title' => 'title',
|
||||
'body' => 'body',
|
||||
'main' => 'main',
|
||||
'base' => 'base',
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -63,14 +80,24 @@ class WireMarkupRegions extends Wire {
|
||||
*
|
||||
*/
|
||||
protected $actions = array(
|
||||
'prepend',
|
||||
'append',
|
||||
'before',
|
||||
'after',
|
||||
'replace',
|
||||
'remove',
|
||||
'prepend' => 'prepend',
|
||||
'append' => 'append',
|
||||
'before' => 'before',
|
||||
'after' => 'after',
|
||||
'replace' => 'replace',
|
||||
'remove' => 'remove',
|
||||
'update' => 'update',
|
||||
);
|
||||
|
||||
/**
|
||||
* Markup snippets that should be removed from final output
|
||||
*
|
||||
* @var array
|
||||
* @since 3.0.250
|
||||
*
|
||||
*/
|
||||
protected $removals = array();
|
||||
|
||||
/**
|
||||
* Locate and return all regions of markup having the given attribute
|
||||
*
|
||||
@@ -737,7 +764,7 @@ class WireMarkupRegions extends Wire {
|
||||
if($name && !isset($attrs[$name])) $attrs[$name] = $val;
|
||||
$tag = rtrim($tag); // remove extra space we added
|
||||
$tagName = strtolower($tagName);
|
||||
$selfClosing = in_array($tagName, $this->selfClosingTags);
|
||||
$selfClosing = isset($this->selfClosingTags[$tagName]);
|
||||
$classes = isset($attrs['class']) ? explode(' ', $attrs['class']) : array();
|
||||
$id = isset($attrs['id']) ? $attrs['id'] : '';
|
||||
$pwid = '';
|
||||
@@ -768,7 +795,7 @@ class WireMarkupRegions extends Wire {
|
||||
} else {
|
||||
$actionTarget = $value;
|
||||
}
|
||||
if($actionTarget && in_array($action, $this->actions)) {
|
||||
if($actionTarget && isset($this->actions[$action])) {
|
||||
// found a valid action and target
|
||||
unset($attrs[$name]);
|
||||
$actionType = $actionTarget === true ? 'bool' : 'attr';
|
||||
@@ -786,7 +813,7 @@ class WireMarkupRegions extends Wire {
|
||||
list($prefix, $action) = explode('-', $class, 2);
|
||||
if(strpos($action, '-')) list($action, $actionTarget) = explode('-', $action, 2);
|
||||
if($prefix && $actionTarget) {} // ignore
|
||||
if(in_array($action, $this->actions)) {
|
||||
if(isset($this->actions[$action])) {
|
||||
// valid action, remove action from classes and class attribute
|
||||
unset($classes[$key]);
|
||||
$attrs['class'] = implode(' ', $classes);
|
||||
@@ -804,6 +831,10 @@ class WireMarkupRegions extends Wire {
|
||||
// if there's an action, but no target, the target is assumed to be the pw-id or id
|
||||
if($action && (!$actionTarget || $actionTarget === true)) $actionTarget = $pwid;
|
||||
|
||||
if(strpos($actionTarget, '^') === 0) {
|
||||
$actionType = 'tag';
|
||||
}
|
||||
|
||||
$info = array(
|
||||
'id' => $id,
|
||||
'pwid' => $pwid ? $pwid : $id,
|
||||
@@ -956,14 +987,38 @@ class WireMarkupRegions extends Wire {
|
||||
$classes = explode(' ', $value);
|
||||
$classes = array_merge($tagInfo['classes'], $classes);
|
||||
$classes = array_unique($classes);
|
||||
// identify remove classes
|
||||
$forceAddClasses = [];
|
||||
$removeMatchClasses = [];
|
||||
// identify force add and remove classes
|
||||
foreach($classes as $key => $class) {
|
||||
if(strpos($class, '-') !== 0) continue;
|
||||
$removeClass = ltrim($class, '-');
|
||||
unset($classes[$key]);
|
||||
while(false !== ($k = array_search($removeClass, $classes))) unset($classes[$k]);
|
||||
if(strpos($class, '+') === 0){
|
||||
$class = ltrim($class, '+');
|
||||
$forceAddClasses[$class] = $class;
|
||||
unset($classes[$key]);
|
||||
} else if(strpos($class, '-') === 0) {
|
||||
$removeClass = substr($class, 1);
|
||||
if(strpos($removeClass, '*') !== false) $removeMatchClasses[] = $removeClass;
|
||||
unset($classes[$key]);
|
||||
while(false !== ($k = array_search($removeClass, $classes))) unset($classes[$k]);
|
||||
}
|
||||
}
|
||||
$attrs['class'] = implode(' ', $classes);
|
||||
if(count($classes) && count($removeMatchClasses)) {
|
||||
foreach($removeMatchClasses as $removeClass) {
|
||||
if(strpos($removeClass, '/') === 0) {
|
||||
// already a regex
|
||||
if(strrpos($removeClass, '/') === 0) $removeClass .= '/';
|
||||
} else {
|
||||
// convert wildcard to regex
|
||||
$removeClass = '/^' . str_replace('\\*', '.+', preg_quote($removeClass, '/')) . '$/';
|
||||
}
|
||||
foreach($classes as $key => $class) {
|
||||
if(preg_match($removeClass, $class)) {
|
||||
unset($classes[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$attrs['class'] = implode(' ', array_merge($forceAddClasses, $classes));
|
||||
} else {
|
||||
// replace
|
||||
$attrs[$name] = $value;
|
||||
@@ -1050,6 +1105,17 @@ class WireMarkupRegions extends Wire {
|
||||
|
||||
$pos = null;
|
||||
|
||||
if($name === 'tag') {
|
||||
if(strpos($value, '.')) {
|
||||
list($tag, $class) = explode('.', $value, 2);
|
||||
if(stripos($html, "<$tag") === false) return false;
|
||||
$value = $class;
|
||||
$name = 'class';
|
||||
} else {
|
||||
return stripos($html, "<$value>") || stripos($html, "<$value ");
|
||||
}
|
||||
}
|
||||
|
||||
if($value === true) {
|
||||
$tests = array(
|
||||
" $name ",
|
||||
@@ -1095,16 +1161,21 @@ class WireMarkupRegions extends Wire {
|
||||
if($name == 'id') {
|
||||
$names = '(id|pw-id|data-pw-id)';
|
||||
} else {
|
||||
$names = preg_quote($name);
|
||||
$names = preg_quote($name, '!');
|
||||
}
|
||||
if($value === true) {
|
||||
// match only the presence of the attribute
|
||||
$regex = '!<[^<>]*\s' . $names . '[=\s/>]!i';
|
||||
} else if($name === 'class') {
|
||||
// match class even if other class names are present
|
||||
$regex = '!<[^<>]*\sclass\s*=\s*["\'][^"\'<>]*\b' . preg_quote($value) . '[\s"\']!i';
|
||||
} else {
|
||||
$regex = '/<[^<>]*\s' . $names . '\s*=\s*["\']?' . preg_quote($value) . '(?:["\']|[\s>])/i';
|
||||
// match attribute value
|
||||
$regex = '!<[^<>]*\s' . $names . '\s*=\s*["\']?' . preg_quote($value) . '(?:["\']|[\s>])!i';
|
||||
}
|
||||
if(preg_match($regex, $html)) $pos = true;
|
||||
}
|
||||
|
||||
|
||||
return $pos !== false;
|
||||
}
|
||||
|
||||
@@ -1141,6 +1212,13 @@ class WireMarkupRegions extends Wire {
|
||||
if(self::debug) {
|
||||
$findOptions['debugNote'] = "update.$options[action]($selector)";
|
||||
}
|
||||
|
||||
// convert to tag matching format for find() method
|
||||
if(strpos($selector, '^') === 0) {
|
||||
$selector = ltrim($selector, '^');
|
||||
// tag is implied if in 'tag.class' format, so only add brackets if no class
|
||||
if(!strpos($selector, '.')) $selector = "<$selector>";
|
||||
}
|
||||
|
||||
$findRegions = $this->find($selector, $markup, $findOptions);
|
||||
|
||||
@@ -1155,10 +1233,11 @@ class WireMarkupRegions extends Wire {
|
||||
if($action == 'auto') {
|
||||
// auto mode delegates to the region action
|
||||
$action = '';
|
||||
if(in_array($region['action'], $this->actions)) $action = $region['action'];
|
||||
if(isset($this->actions[$region['action']])) $action = $region['action'];
|
||||
}
|
||||
|
||||
switch($action) {
|
||||
case 'update':
|
||||
case 'append':
|
||||
$replacement = $region['open'] . $region['region'] . $content . $region['close'];
|
||||
break;
|
||||
@@ -1273,7 +1352,30 @@ class WireMarkupRegions extends Wire {
|
||||
$options['action'] = 'after'; // after intended
|
||||
return $this->replace($selector, '', $markup, $options);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize given HTML for markup regions
|
||||
*
|
||||
* @param string $html
|
||||
* @since 3.0.250
|
||||
*
|
||||
*/
|
||||
protected function initHtml(&$html) {
|
||||
$tests = [ '="<', "='<", '="<', "='<" ];
|
||||
foreach($tests as $test) {
|
||||
$apply = strpos($html, $test);
|
||||
if($apply) break;
|
||||
}
|
||||
if($apply) {
|
||||
$actions = implode('|', $this->actions);
|
||||
$html = preg_replace(
|
||||
'!(<[^<>]+\s(?:data-pw-|pw-)(?:' . $actions . ')=["\'])(?:<|<)([^<>\'"&]+)(?:>|>)(["\'])!i',
|
||||
'$1^$2$3',
|
||||
$html
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify and populate markup regions in given HTML
|
||||
*
|
||||
@@ -1355,16 +1457,20 @@ class WireMarkupRegions extends Wire {
|
||||
$defaults = array(
|
||||
'useClassActions' => false // allow use of "pw-*" class actions? (legacy)
|
||||
);
|
||||
|
||||
if(is_string($htmlRegions) && $recursionLevel === 1) {
|
||||
$this->initHtml($htmlRegions);
|
||||
}
|
||||
|
||||
$options = array_merge($defaults, $options);
|
||||
$leftoverMarkup = '';
|
||||
$hasDebugLandmark = strpos($htmlDocument, self::debugLandmark) !== false;
|
||||
$debug = $hasDebugLandmark && $this->wire()->config->debug;
|
||||
$debugTimer = $debug ? Debug::timer() : 0;
|
||||
$this->populateSingles($htmlDocument, $htmlRegions);
|
||||
|
||||
if(is_array($htmlRegions)) {
|
||||
$regions = $htmlRegions;
|
||||
$leftoverMarkup = '';
|
||||
|
||||
} else if($this->hasRegions($htmlRegions)) {
|
||||
$htmlRegions = $this->stripRegions('<!--', $htmlRegions);
|
||||
@@ -1406,14 +1512,39 @@ class WireMarkupRegions extends Wire {
|
||||
|
||||
// $xregion = $region;
|
||||
$action = $region['action'];
|
||||
$actionTarget = $region['actionTarget'];
|
||||
$actionType = $region['actionType'];
|
||||
$actionTarget = ltrim($region['actionTarget']);
|
||||
|
||||
if(strpos($actionTarget, '.') === 0) {
|
||||
$actionAttribute = 'class';
|
||||
$actionTargetPrefix = '.';
|
||||
} else if(strpos($actionTarget, '^') === 0) {
|
||||
$actionAttribute = 'tag';
|
||||
$actionTargetPrefix = '^';
|
||||
} else {
|
||||
$actionAttribute = 'id';
|
||||
$actionTargetPrefix = '#';
|
||||
}
|
||||
|
||||
$actionTarget = ltrim($actionTarget, '.#^');
|
||||
$regionHTML = $region['region'];
|
||||
$mergeAttr = $region['attrs'];
|
||||
|
||||
unset($mergeAttr['id']);
|
||||
$documentHasTarget = $this->hasAttribute('id', $actionTarget, $htmlDocument);
|
||||
$isNew = ($region['actionType'] == 'attr' && $region['action'] != 'replace');
|
||||
if(!$isNew) $isNew = $action == 'before' || $action == 'after';
|
||||
$documentHasTarget = $this->hasAttribute($actionAttribute, $actionTarget, $htmlDocument);
|
||||
if(!$documentHasTarget) {
|
||||
// if target was not matched, check for target as a single tag (html, head, body, main)
|
||||
if(isset($this->singles[$actionTarget])) {
|
||||
$actionTarget = "pwmr-$actionTarget";
|
||||
$documentHasTarget = $this->hasAttribute('data-pw-id', $actionTarget, $htmlDocument);
|
||||
}
|
||||
}
|
||||
|
||||
if($actionType === 'attr' || $actionType === 'tag') {
|
||||
$isNew = $action != 'replace' && $action != 'update';
|
||||
} else {
|
||||
$isNew = $action === 'before' || $action === 'after';
|
||||
}
|
||||
|
||||
if($isNew) {
|
||||
// element is newly added element not already present
|
||||
@@ -1422,7 +1553,7 @@ class WireMarkupRegions extends Wire {
|
||||
$attrs = $region['attrs'];
|
||||
$attrStr = count($attrs) ? ' ' . $this->renderAttributes($attrs, false) : '';
|
||||
if(!strlen(trim($attrStr))) $attrStr = '';
|
||||
if($region['actionType'] == 'bool') {
|
||||
if($actionType == 'bool') {
|
||||
$regionHTML = $region['region'];
|
||||
} else {
|
||||
$regionHTML = str_replace($region['open'], "<$region[name]$attrStr>", $regionHTML);
|
||||
@@ -1436,7 +1567,7 @@ class WireMarkupRegions extends Wire {
|
||||
$pwid = empty($region['pwid']) ? $region['actionTarget'] : $region['pwid'];
|
||||
$open = $region['open'];
|
||||
$openLen = strlen($open);
|
||||
if($openLen > 50) $open = substr($open, 0, 30) . '[sm]... +' . ($openLen - 30) . ' bytes[/sm]>';
|
||||
if($openLen > 100) $open = substr($open, 0, 100) . '[sm]... +' . ($openLen - 100) . ' bytes[/sm]>';
|
||||
$debugRegionStart = "[sm]" . trim(substr($region['region'], 0, 80));
|
||||
$pos = strrpos($debugRegionStart, '>');
|
||||
if($pos) $debugRegionStart = substr($debugRegionStart, 0, $pos+1);
|
||||
@@ -1444,8 +1575,21 @@ class WireMarkupRegions extends Wire {
|
||||
//$debugRegionEnd = substr($region['region'], -30);
|
||||
//$pos = strpos($debugRegionEnd, '</');
|
||||
//if($pos !== false) $debugRegionEnd = substr($debugRegionEnd, $pos);
|
||||
$region['note'] = strtoupper($debugAction) . " [b]#{$pwid}[/b] " .
|
||||
($region['actionTarget'] != $pwid ? "(target=$region[actionTarget])" : "") .
|
||||
if(strpos($open, 'pw-')) {
|
||||
$open = preg_replace('!\s(data-)?pw-(' . implode('|', $this->actions) . ')(=[^\s><]+)?!', '', $open);
|
||||
}
|
||||
if(strpos($pwid, 'pwmr-') === 0) {
|
||||
$pwid = '<' . substr($pwid, 5) . '…';
|
||||
$debugActionTarget = $pwid;
|
||||
} else if($actionTargetPrefix === '^') {
|
||||
$pwid = "<" . ltrim($pwid, '^') . '…';
|
||||
$debugActionTarget = $pwid;
|
||||
} else {
|
||||
$pwid = $actionTargetPrefix . $pwid;
|
||||
$debugActionTarget = $actionTargetPrefix . $actionTarget;
|
||||
}
|
||||
$region['note'] = strtoupper($debugAction) . " {$pwid} " .
|
||||
($debugActionTarget != $pwid ? "(target=$debugActionTarget) " : "") .
|
||||
"[sm]with[/sm] $open";
|
||||
if($region['close']) {
|
||||
$region['note'] .= $this->debugNoteStr($debugRegionStart) . $region['close'];
|
||||
@@ -1464,7 +1608,7 @@ class WireMarkupRegions extends Wire {
|
||||
} else {
|
||||
// update the markup
|
||||
$updates[] = array(
|
||||
'actionTarget' => "#$actionTarget",
|
||||
'actionTarget' => $actionTargetPrefix . $actionTarget,
|
||||
'regionHTML' => $regionHTML,
|
||||
'action' => $action,
|
||||
'mergeAttr' => $mergeAttr,
|
||||
@@ -1559,6 +1703,41 @@ class WireMarkupRegions extends Wire {
|
||||
return $numUpdates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate single-use tags as unnamed markup regions
|
||||
*
|
||||
* @param string $htmlDocument
|
||||
* @param array|string $htmlRegions
|
||||
* @since 3.0.250
|
||||
*
|
||||
*/
|
||||
protected function populateSingles(&$htmlDocument, &$htmlRegions) {
|
||||
|
||||
foreach($this->singles as $tag) {
|
||||
|
||||
$attr = "data-pw-id=\"pwmr-$tag\"";
|
||||
$find = [ "<$tag>", "<$tag " ];
|
||||
$replace = [ "<$tag $attr>", "<$tag $attr " ];
|
||||
$has = false;
|
||||
|
||||
if(is_array($htmlRegions)) {
|
||||
foreach($htmlRegions as $key => $htmlRegion) {
|
||||
if(stripos($htmlRegion, "<$tag") === false) continue;
|
||||
$htmlRegions[$key] = str_ireplace($find, $replace, $htmlRegion);
|
||||
$has = true;
|
||||
}
|
||||
} else if(strpos($htmlRegions, "<$tag") !== false) {
|
||||
$htmlRegions = str_ireplace($find, $replace, $htmlRegions);
|
||||
$has = true;
|
||||
}
|
||||
|
||||
if($has || stripos($htmlDocument, "<$tag") !== false) {
|
||||
$htmlDocument = str_ireplace($find, $replace, $htmlDocument);
|
||||
$this->removals[] = " $attr";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any <region> or <pw-region> tags present in the markup, leaving their innerHTML contents
|
||||
*
|
||||
@@ -1577,9 +1756,25 @@ class WireMarkupRegions extends Wire {
|
||||
$updated = true;
|
||||
}
|
||||
|
||||
if(count($this->removals)) {
|
||||
$qty = 0;
|
||||
$html = str_ireplace($this->removals, '', $html, $qty);
|
||||
if($qty) $updated = true;
|
||||
}
|
||||
|
||||
if(stripos($html, ' data-pw-id=') || stripos($html, ' pw-id=')) {
|
||||
$html = preg_replace('/(<[^>]+)(?: data-pw-id| pw-id)=["\']?[^>\s"\']+["\']?/i', '$1', $html);
|
||||
$updated = true;
|
||||
$find = [];
|
||||
$replace = [];
|
||||
if(preg_match_all('/(<[^<>]+?)(?: data-pw-id=| pw-id=)["\']?[^>\s"\']+["\']?/i', $html, $matches)) {
|
||||
foreach($matches[0] as $key => $fullMatch) {
|
||||
$find[] = $fullMatch;
|
||||
$replace[] = $matches[1][$key];
|
||||
}
|
||||
}
|
||||
if(count($find)) {
|
||||
$html = str_ireplace($find, $replace, $html);
|
||||
$updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $updated;
|
||||
@@ -1622,7 +1817,7 @@ class WireMarkupRegions extends Wire {
|
||||
|
||||
protected function debugNoteStr($str, $maxLength = 0) {
|
||||
$str = str_replace(array("\r", "\n", "\t"), ' ', $str);
|
||||
while(strpos($str, ' ') !== false) $str= str_replace(' ', ' ', $str);
|
||||
while(strpos($str, ' ') !== false) $str = str_replace(' ', ' ', $str);
|
||||
if($maxLength) $str = substr($str, 0, $maxLength);
|
||||
return trim($str);
|
||||
}
|
||||
@@ -1631,6 +1826,7 @@ class WireMarkupRegions extends Wire {
|
||||
if(!count($debugNotes)) $debugNotes[] = "Nothing found";
|
||||
if($debugTimer !== null) $debugNotes[] = '[sm]' . Debug::timer($debugTimer) . ' seconds[/sm]';
|
||||
$out = "• " . implode("\n• ", $debugNotes);
|
||||
$out = str_replace($this->removals, '', $out);
|
||||
$out = $this->wire()->sanitizer->entities($out);
|
||||
$out = str_replace(array('[sm]', '[/sm]'), array('<small style="opacity:0.7">', '</small>'), $out);
|
||||
$out = str_replace(array('[b]', '[/b]'), array('<strong>', '</strong>'), $out);
|
||||
|
@@ -302,6 +302,60 @@ class WireRandom extends Wire {
|
||||
return $this->alphanumeric($length, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random string using given characters
|
||||
*
|
||||
* @param int $length Length of string or specify 0 for random length
|
||||
* @param string $characters Charaacters to use for random string or omit for partial ASCII set
|
||||
* @param array $options
|
||||
* - `minLength` (int): Minimum allowed length if length argument is 0 (default=10)
|
||||
* - `maxLength` (int): Maximum allowed length if length argument is 0 (default=40)
|
||||
* - `fast` (bool): Use a faster randomization method? (default=false)
|
||||
* @return string
|
||||
* @since 3.0.251
|
||||
*
|
||||
*/
|
||||
public function string($length = 0, $characters = '', array $options = []) {
|
||||
|
||||
$defaults = [
|
||||
'minLength' => 10,
|
||||
'maxLength' => 40,
|
||||
'fast' => false,
|
||||
];
|
||||
|
||||
$options = array_merge($defaults, $options);
|
||||
|
||||
if(empty($characters)) {
|
||||
$characters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
$characters .= strtoupper($characters);
|
||||
$characters .= '0123456789';
|
||||
$characters .= '-_;:/.,!@$%^*()-+~|';
|
||||
}
|
||||
|
||||
if($length < 1) {
|
||||
if($options['fast']) {
|
||||
$length = mt_rand($options['minLength'], $options['maxLength']);
|
||||
} else {
|
||||
$length = $this->integer($options['minLength'], $options['maxLength']);
|
||||
}
|
||||
}
|
||||
|
||||
$str = '';
|
||||
$L = strlen($characters) - 1;
|
||||
|
||||
for($n = 0; $n < $length; $n++) {
|
||||
if($options['fast']) {
|
||||
$v = mt_rand(0, $L);
|
||||
} else {
|
||||
$v = $this->integer(0, $L);
|
||||
}
|
||||
$c = substr($characters, $v, 1);
|
||||
$str .= $c;
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a random integer
|
||||
*
|
||||
|
@@ -6,7 +6,7 @@
|
||||
* Wire Data Access Object, provides reusable capability for loading, saving, creating, deleting,
|
||||
* and finding items of descending class-defined types.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2024 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @method WireArray load(WireArray $items, $selectors = null)
|
||||
@@ -220,7 +220,9 @@ abstract class WireSaveableItems extends Wire implements \IteratorAggregate {
|
||||
$query->execute();
|
||||
$rows = $query->fetchAll(\PDO::FETCH_ASSOC);
|
||||
$n = 0;
|
||||
|
||||
|
||||
$this->loadRowsReady($rows);
|
||||
|
||||
foreach($rows as $row) {
|
||||
if($useLazy) {
|
||||
$this->lazyItems[$n] = $row;
|
||||
@@ -238,6 +240,14 @@ abstract class WireSaveableItems extends Wire implements \IteratorAggregate {
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after rows loaded from DB but before populated to this instance
|
||||
*
|
||||
* @param array $rows
|
||||
*
|
||||
*/
|
||||
protected function loadRowsReady(array &$rows) { }
|
||||
|
||||
/**
|
||||
* Create a new Saveable item from a raw array ($row) and add it to $items
|
||||
*
|
||||
@@ -247,7 +257,7 @@ abstract class WireSaveableItems extends Wire implements \IteratorAggregate {
|
||||
* @since 3.0.194
|
||||
*
|
||||
*/
|
||||
protected function initItem(array &$row, WireArray $items = null) {
|
||||
protected function initItem(array &$row, ?WireArray $items = null) {
|
||||
|
||||
if(!empty($row['data'])) {
|
||||
if(is_string($row['data'])) $row['data'] = $this->decodeData($row['data']);
|
||||
@@ -753,7 +763,7 @@ abstract class WireSaveableItems extends Wire implements \IteratorAggregate {
|
||||
* @return WireLog
|
||||
*
|
||||
*/
|
||||
public function log($str, Saveable $item = null) {
|
||||
public function log($str, ?Saveable $item = null) {
|
||||
$logs = $this->wire()->config->logs;
|
||||
$name = $this->className(array('lowercase' => true));
|
||||
if($logs && in_array($name, $logs)) {
|
||||
|
@@ -109,7 +109,7 @@ abstract class WireSaveableItemsLookup extends WireSaveableItems {
|
||||
* @since 3.0.194
|
||||
*
|
||||
*/
|
||||
protected function initItem(array &$row, WireArray $items = null) {
|
||||
protected function initItem(array &$row, ?WireArray $items = null) {
|
||||
|
||||
$lookupField = $this->getLookupField();
|
||||
$lookupValue = $row[$lookupField];
|
||||
|
@@ -90,6 +90,25 @@ class WireShutdown extends Wire {
|
||||
*/
|
||||
protected $error = array();
|
||||
|
||||
/**
|
||||
* Methods that should have their arguments suppressed from PHP backtraces
|
||||
*
|
||||
* - Each method must include a `->`.
|
||||
* - Methods should not include parenthesis.
|
||||
* - If for specific class, include the class name before the `->`.
|
||||
*
|
||||
* @var string[]
|
||||
*
|
||||
*/
|
||||
protected $banBacktraceMethods = array(
|
||||
'->___login', // Session or ProcessLogin
|
||||
'->___start', // i.e. Tfa
|
||||
'->___setPass', // Password.php
|
||||
'Session->___authenticate',
|
||||
'Password->matches',
|
||||
'Password->hash',
|
||||
);
|
||||
|
||||
/**
|
||||
* Default HTML to use for error message
|
||||
*
|
||||
@@ -158,7 +177,7 @@ class WireShutdown extends Wire {
|
||||
E_USER_ERROR => $this->_('Error'),
|
||||
E_USER_WARNING => $this->_('User Warning'),
|
||||
E_USER_NOTICE => $this->_('User Notice'),
|
||||
E_STRICT => $this->_('Strict Warning'),
|
||||
2048 => $this->_('Strict Warning'), // 2048=E_STRICT (deprecated in PHP 8.4)
|
||||
E_RECOVERABLE_ERROR => $this->_('Recoverable Fatal Error')
|
||||
);
|
||||
|
||||
@@ -188,6 +207,7 @@ class WireShutdown extends Wire {
|
||||
protected function getErrorMessage(array $error) {
|
||||
|
||||
$type = $error['type'];
|
||||
$config = $this->config;
|
||||
|
||||
if(isset($this->types[$type])) {
|
||||
$errorType = $this->types[$type];
|
||||
@@ -203,7 +223,25 @@ class WireShutdown extends Wire {
|
||||
$detail = '';
|
||||
}
|
||||
|
||||
return "$errorType: \t$message $detail ";
|
||||
$message = "$errorType: \t$message $detail ";
|
||||
|
||||
if(strpos($message, '#1') !== false && stripos($message, '):')) {
|
||||
// backtrace likely present in $message
|
||||
// methods that should have their arguments excluded from backtrace
|
||||
foreach($this->banBacktraceMethods as $name) {
|
||||
if(strpos($message, "$name(") === false) continue;
|
||||
if(!preg_match_all('!' . $name . '\([^\n]+\)!', $message, $matches)) continue;
|
||||
foreach($matches[0] as $match) {
|
||||
$message = str_replace($match, '->' . $name . '(...)', $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(strlen((string) $config->dbPass) > 4) {
|
||||
$message = str_replace((string) $config->dbPass, '[...]', $message);
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,8 +251,7 @@ class WireShutdown extends Wire {
|
||||
*
|
||||
*/
|
||||
protected function getWireInput() {
|
||||
/** @var WireInput $input */
|
||||
$input = $this->wire('input');
|
||||
$input = $this->wire()->input;
|
||||
if($input) return $input;
|
||||
$input = $this->wire(new WireInput());
|
||||
return $input;
|
||||
@@ -228,8 +265,7 @@ class WireShutdown extends Wire {
|
||||
*/
|
||||
protected function getCurrentUrl() {
|
||||
|
||||
/** @var Page|null $page */
|
||||
$page = $this->wire('page');
|
||||
$page = $this->wire()->page;
|
||||
$input = $this->getWireInput();
|
||||
$http = isset($_SERVER['HTTP_HOST']) || isset($_SERVER['REQUEST_URI']);
|
||||
|
||||
@@ -552,9 +588,12 @@ class WireShutdown extends Wire {
|
||||
if($useHTML && $config->ajax) $useHTML = false;
|
||||
|
||||
// include IP address is user name if configured to do so
|
||||
if($config->logIP && $this->wire('session')) {
|
||||
$ip = $this->wire('session')->getIP();
|
||||
if(strlen($ip)) $name = "$name ($ip)";
|
||||
if($config->logIP) {
|
||||
$session = $this->wire()->session;
|
||||
if($session) {
|
||||
$ip = $session->getIP();
|
||||
if(strlen($ip)) $name = "$name ($ip)";
|
||||
}
|
||||
}
|
||||
|
||||
// save to errors.txt log file
|
||||
@@ -781,7 +820,7 @@ class WireShutdown extends Wire {
|
||||
public function shutdownExternal() {
|
||||
if(error_get_last()) return;
|
||||
/** @var ProcessPageView $process */
|
||||
$process = $this->wire('process');
|
||||
$process = $this->wire()->process;
|
||||
if($process == 'ProcessPageView') $process->finished();
|
||||
}
|
||||
}
|
||||
|
@@ -72,6 +72,11 @@ class WireTempDir extends Wire {
|
||||
if(!is_null($this->tempDirRoot)) throw new WireException("Temp dir has already been created");
|
||||
if(empty($name)) $name = $this->createName();
|
||||
if(is_object($name)) $name = wireClassName($name, false);
|
||||
|
||||
if($basePath && !$this->wire()->files->allowPath($basePath, true)) {
|
||||
$this->log("Given base path $basePath is not within ProcessWire assets so has been replaced");
|
||||
$basePath = '';
|
||||
}
|
||||
|
||||
$basePath = $this->classRootPath(true, $basePath);
|
||||
$this->classRoot = $basePath;
|
||||
|
@@ -905,7 +905,7 @@ class WireTextTools extends Wire {
|
||||
* - `has` (bool): Specify true to only return true or false if it has tags (default=false).
|
||||
* - `tagOpen` (string): The required opening tag character(s), default is '{'
|
||||
* - `tagClose` (string): The required closing tag character(s), default is '}'
|
||||
* @return array|bool
|
||||
* @return array|bool Always returns array unless you specify the `has` option as true.
|
||||
* @since 3.0.126
|
||||
*
|
||||
*/
|
||||
@@ -918,6 +918,7 @@ class WireTextTools extends Wire {
|
||||
);
|
||||
|
||||
$options = array_merge($defaults, $options);
|
||||
$str = (string) $str;
|
||||
$tags = array();
|
||||
$pos1 = strpos($str, $options['tagOpen']);
|
||||
|
||||
@@ -981,7 +982,8 @@ class WireTextTools extends Wire {
|
||||
* - `tagOpen` (string): The required opening tag character(s), default is '{'
|
||||
* - `tagClose` (string): The optional closing tag character(s), default is '}'
|
||||
* - `recursive` (bool): If replacement value contains tags, populate those too? (default=false)
|
||||
* - `removeNullTags` (bool): If a tag resolves to a NULL, remove it? If false, tag will remain. (default=true)
|
||||
* - `removeNullTags` (bool): If a tag resolves to a NULL (i.e. field not present), remove it? (default=true)
|
||||
* - `removeEmptyTags` (bool): If a tag value resolves to blank string, false or NULL, remove it? (default=true) 3.0.237+
|
||||
* - `entityEncode` (bool): Entity encode the values pulled from $vars? (default=false)
|
||||
* - `entityDecode` (bool): Entity decode the values pulled from $vars? (default=false)
|
||||
* - `allowMarkup` (bool): Allow markup to appear in populated variables? (default=true)
|
||||
@@ -995,7 +997,8 @@ class WireTextTools extends Wire {
|
||||
'tagOpen' => '{', // opening tag (required)
|
||||
'tagClose' => '}', // closing tag (optional)
|
||||
'recursive' => false, // if replacement value contains tags, populate those too?
|
||||
'removeNullTags' => true, // if a tag value resolves to a NULL, remove it? If false, tag will be left in tact.
|
||||
'removeNullTags' => true, // If a tag resolves to a NULL (i.e. field not present on page), remove it?
|
||||
'removeEmptyTags' => true, // If a tag value resolves to blank string, false or null, remove it?
|
||||
'entityEncode' => false, // entity encode values pulled from $vars?
|
||||
'entityDecode' => false, // entity decode values pulled from $vars?
|
||||
'allowMarkup' => true, // allow markup to appear in populated variables?
|
||||
@@ -1015,6 +1018,7 @@ class WireTextTools extends Wire {
|
||||
if(is_object($vars)) {
|
||||
if($vars instanceof Page) {
|
||||
$fieldValue = $options['allowMarkup'] ? $vars->getMarkup($fieldName) : $vars->getText($fieldName);
|
||||
if($fieldValue === '' && $vars->get($fieldName) === null) $fieldValue = null;
|
||||
} else if($vars instanceof WireData) {
|
||||
$fieldValue = $vars->get($fieldName);
|
||||
} else {
|
||||
@@ -1023,6 +1027,9 @@ class WireTextTools extends Wire {
|
||||
} else if(is_array($vars)) {
|
||||
$fieldValue = isset($vars[$fieldName]) ? $vars[$fieldName] : null;
|
||||
}
|
||||
|
||||
// if value resolves to empty and we are not removing empty tags, then do not add to replacements
|
||||
if(empty($fieldValue) && !strlen("$fieldValue") && !$options['removeEmptyTags']) continue;
|
||||
|
||||
// if value resolves to null and we are not removing null tags, then do not add to replacements
|
||||
if($fieldValue === null && !$options['removeNullTags']) continue;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user