Merge branch '3.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / package / Package.class.php
CommitLineData
11ade432
AE
1<?php
2namespace wcf\data\package;
3use wcf\data\DatabaseObject;
a17de04e 4use wcf\system\package\PackageInstallationDispatcher;
11ade432
AE
5use wcf\system\WCF;
6use wcf\util\FileUtil;
7
8/**
9 * Represents a package.
a17de04e 10 *
11ade432 11 * @author Alexander Ebert
c839bd49 12 * @copyright 2001-2018 WoltLab GmbH
11ade432 13 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
e71525e4 14 * @package WoltLabSuite\Core\Data\Package
e9335ed9 15 *
ed6a4e42
MS
16 * @property-read integer $packageID unique id of the package
17 * @property-read string $package unique textual identifier of the package
18 * @property-read string $packageDir relative directory to Core in which the application is installed or empty if package is no application or Core
19 * @property-read string $packageName name of the package or name of language item which contains the name
20 * @property-read string $packageDescription description of the package or name of language item which contains the description
21 * @property-read string $packageVersion installed version of package
22 * @property-read integer $packageDate timestamp at which the installed package version has been released
23 * @property-read integer $installDate timestamp at which the package has been installed
24 * @property-read integer $updateDate timestamp at which the package has been updated or installed if it has not been updated yet
25 * @property-read string $packageURL external url to website with more information about the package
26 * @property-read integer $isApplication is `1` if the package delivers an application, otherwise `0`
27 * @property-read string $author author of the package
28 * @property-read string $authorURL external url to the website of the package author
11ade432
AE
29 */
30class Package extends DatabaseObject {
48050873
MS
31 /**
32 * list of packages that this package requires
7a23a706 33 * @var Package[]
48050873
MS
34 */
35 protected $dependencies = null;
36
38bbf0b4
MS
37 /**
38 * list of packages that require this package
7a23a706 39 * @var Package[]
38bbf0b4
MS
40 */
41 protected $dependentPackages = null;
42
11ade432
AE
43 /**
44 * installation directory
11ade432
AE
45 * @var string
46 */
47 protected $dir = '';
48
48050873
MS
49 /**
50 * list of packages that were given as required packages during installation
7a23a706 51 * @var Package[]
48050873
MS
52 */
53 protected $requiredPackages = null;
54
490ef799 55 /**
94db4109 56 * list of ids of packages which are required by another package
7a23a706 57 * @var integer[]
490ef799 58 */
6cbea15c
MS
59 protected static $requiredPackageIDs = null;
60
61 /**
62 * package requirements
63 * @var array
64 */
490ef799
MS
65 protected static $requirements = null;
66
11ade432 67 /**
28410a97 68 * Returns true if this package is required by other packages.
9f959ced 69 *
11ade432
AE
70 * @return boolean
71 */
72 public function isRequired() {
490ef799 73 self::loadRequirements();
11ade432 74
6cbea15c 75 return in_array($this->packageID, self::$requiredPackageIDs);
11ade432
AE
76 }
77
78 /**
79 * Returns true if package is a plugin.
9f959ced 80 *
11ade432
AE
81 * @return boolean
82 */
83 public function isPlugin() {
b8741db6
AE
84 if ($this->isApplication) {
85 return false;
86 }
11ade432 87
b8741db6 88 return true;
11ade432
AE
89 }
90
91 /**
92 * Returns the name of this package.
9f959ced 93 *
11ade432
AE
94 * @return string
95 */
96 public function getName() {
07c78f25
AE
97 return WCF::getLanguage()->get($this->packageName);
98 }
99
100 /**
0fcfe5f6 101 * @inheritDoc
07c78f25
AE
102 */
103 public function __toString() {
104 return $this->getName();
11ade432
AE
105 }
106
11ade432
AE
107 /**
108 * Returns the abbreviation of the package name.
9f959ced 109 *
11ade432
AE
110 * @param string $package
111 * @return string
112 */
113 public static function getAbbreviation($package) {
114 $array = explode('.', $package);
115 return array_pop($array);
116 }
117
11ade432 118 /**
38bbf0b4
MS
119 * Returns the list of packages which are required by this package. The
120 * returned packages are the packages given in the <requiredpackages> tag
121 * in the package.xml of this package.
9f959ced 122 *
7a23a706 123 * @return Package[]
11ade432
AE
124 */
125 public function getRequiredPackages() {
48050873 126 if ($this->requiredPackages === null) {
38bbf0b4 127 self::loadRequirements();
48050873 128
058cbd6a 129 $this->requiredPackages = [];
38bbf0b4
MS
130 if (isset(self::$requirements[$this->packageID])) {
131 foreach (self::$requirements[$this->packageID] as $packageID) {
132 $this->requiredPackages[$packageID] = PackageCache::getInstance()->getPackage($packageID);
133 }
48050873 134 }
11ade432
AE
135 }
136
48050873 137 return $this->requiredPackages;
11ade432
AE
138 }
139
5ccce215 140 /**
28410a97 141 * Returns true if current user can uninstall this package.
5ccce215
AE
142 *
143 * @return boolean
144 */
145 public function canUninstall() {
6476e7a1 146 if (!WCF::getSession()->getPermission('admin.configuration.package.canUninstallPackage')) {
5ccce215
AE
147 return false;
148 }
149
74622428
AE
150 // disallow uninstallation of WCF
151 if ($this->package == 'com.woltlab.wcf') {
5ccce215
AE
152 return false;
153 }
154
cd0fbe9a 155 // check if package is required by another package
4879676b 156 if ($this->isRequired()) {
5ccce215
AE
157 return false;
158 }
159
160 return true;
161 }
162
e852aa82
AE
163 /**
164 * Returns a list of packages dependent from current package.
165 *
7a23a706 166 * @return Package[]
e852aa82
AE
167 */
168 public function getDependentPackages() {
38bbf0b4
MS
169 if ($this->dependentPackages === null) {
170 self::loadRequirements();
171
058cbd6a 172 $this->dependentPackages = [];
38bbf0b4
MS
173 foreach (self::$requirements as $packageID => $requiredPackageIDs) {
174 if (in_array($this->packageID, $requiredPackageIDs)) {
175 $this->dependentPackages[$packageID] = PackageCache::getInstance()->getPackage($packageID);
176 }
e852aa82
AE
177 }
178 }
179
38bbf0b4 180 return $this->dependentPackages;
e852aa82
AE
181 }
182
39935629
AE
183 /**
184 * Overwrites current package version.
185 *
186 * DO NOT call this method outside the package installation!
187 *
188 * @param string $packageVersion
189 */
190 public function setPackageVersion($packageVersion) {
191 $this->data['packageVersion'] = $packageVersion;
192 }
193
9078d83e
MS
194 /**
195 * Returns the absolute path to the package directory with a trailing slash.
196 *
197 * @return string
198 */
199 public function getAbsolutePackageDir() {
200 return FileUtil::addTrailingSlash(FileUtil::getRealPath(WCF_DIR . $this->packageDir));
201 }
202
5ccce215 203 /**
cd0fbe9a 204 * Loads package requirements.
5ccce215 205 */
cd0fbe9a 206 protected static function loadRequirements() {
490ef799 207 if (self::$requirements === null) {
5ccce215 208 $sql = "SELECT packageID, requirement
cd0fbe9a 209 FROM wcf".WCF_N."_package_requirement";
5ccce215
AE
210 $statement = WCF::getDB()->prepareStatement($sql);
211 $statement->execute();
212
058cbd6a
MS
213 self::$requiredPackageIDs = [];
214 self::$requirements = [];
5ccce215 215 while ($row = $statement->fetchArray()) {
cd0fbe9a 216 if (!isset(self::$requirements[$row['packageID']])) {
058cbd6a 217 self::$requirements[$row['packageID']] = [];
5ccce215
AE
218 }
219
cd0fbe9a 220 self::$requirements[$row['packageID']][] = $row['requirement'];
6cbea15c
MS
221
222 if (!in_array($row['requirement'], self::$requiredPackageIDs)) {
223 self::$requiredPackageIDs[] = $row['requirement'];
224 }
5ccce215
AE
225 }
226 }
227 }
228
100859d8 229 /**
28410a97 230 * Returns true if package identified by $package is already installed.
100859d8
AE
231 *
232 * @param string $package
233 * @return boolean
234 */
235 public static function isAlreadyInstalled($package) {
5c6ddd85 236 $sql = "SELECT COUNT(*)
100859d8
AE
237 FROM wcf".WCF_N."_package
238 WHERE package = ?";
239 $statement = WCF::getDB()->prepareStatement($sql);
058cbd6a 240 $statement->execute([$package]);
100859d8 241
5c6ddd85 242 return $statement->fetchSingleColumn() > 0;
100859d8
AE
243 }
244
11ade432
AE
245 /**
246 * Checks if a package name is valid.
a17de04e
MS
247 *
248 * A valid package name begins with at least one alphanumeric character
249 * or an underscore, followed by a dot, followed by at least one alphanumeric
250 * character or an underscore and the same again, possibly repeatedly.
e48ab932
S
251 * The package name cannot be any longer than 191 characters in total due to
252 * internal database character encoding limitations.
9a5d32b2
MS
253 * Example:
254 * com.woltlab.wcf
a17de04e
MS
255 *
256 * Reminder: The package name being examined here contains the 'name' attribute
257 * of the 'package' tag noted in the 'packages.xml' file delivered inside
258 * the respective package.
9f959ced 259 *
39bea7dd
MS
260 * @param string $packageName
261 * @return boolean isValid
11ade432
AE
262 */
263 public static function isValidPackageName($packageName) {
e48ab932
S
264 if (mb_strlen($packageName) < 3 || mb_strlen($packageName) > 191) {
265 return false;
266 }
267
11ade432
AE
268 return preg_match('%^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$%', $packageName);
269 }
270
b4cbf821 271 /**
28410a97 272 * Returns true if package version is valid.
b4cbf821 273 *
9a5d32b2
MS
274 * Examples of valid package versions:
275 * 1.0.0 pl 3
276 * 4.0.0 Alpha 1
277 * 3.1.7 rC 4
b4cbf821
AE
278 *
279 * @param string $version
280 * @return boolean
281 */
282 public static function isValidVersion($version) {
283 return preg_match('~^([0-9]+)\.([0-9]+)\.([0-9]+)(\ (a|alpha|b|beta|d|dev|rc|pl)\ ([0-9]+))?$~is', $version);
284 }
285
da89969e 286 /**
a17de04e
MS
287 * Checks the version number of the installed package against the "fromversion"
288 * number of the update.
289 *
5a095676
MW
290 * The "fromversion" number may contain wildcards (asterisks) which means
291 * that the update covers the whole range of release numbers where the asterisk
292 * wildcards digits from 0 to 9.
293 * For example, if "fromversion" is "1.1.*" and this package updates to
294 * version 1.2.0, all releases from 1.1.0 to 1.1.9 may be updated using
295 * this package.
296 *
da89969e
MW
297 * @param string $currentVersion
298 * @param string $fromVersion
299 * @return boolean
300 */
ac52543a 301 public static function checkFromversion($currentVersion, $fromVersion) {
5a095676
MW
302 if (mb_strpos($fromVersion, '*') !== false) {
303 // from version with wildcard
304 // use regular expression
305 $fromVersion = str_replace('\*', '.*', preg_quote($fromVersion, '!'));
306 if (preg_match('!^'.$fromVersion.'$!i', $currentVersion)) {
307 return true;
308 }
309 }
310 else {
311 if (self::compareVersion($currentVersion, $fromVersion, '=')) {
312 return true;
313 }
da89969e
MW
314 }
315
316 return false;
317 }
318
11ade432
AE
319 /**
320 * Compares two version number strings.
9f959ced 321 *
da89969e
MW
322 * @param string $version1
323 * @param string $version2
324 * @param string $operator
325 * @return boolean result
0c166126 326 * @see http://www.php.net/manual/en/function.version-compare.php
11ade432
AE
327 */
328 public static function compareVersion($version1, $version2, $operator = null) {
329 $version1 = self::formatVersionForCompare($version1);
330 $version2 = self::formatVersionForCompare($version2);
331 if ($operator === null) return version_compare($version1, $version2);
332 else return version_compare($version1, $version2, $operator);
333 }
334
335 /**
336 * Formats a package version string for comparing.
9f959ced 337 *
11ade432 338 * @param string $version
39bea7dd 339 * @return string formatted version
0c166126 340 * @see http://www.php.net/manual/en/function.version-compare.php
11ade432
AE
341 */
342 private static function formatVersionForCompare($version) {
343 // remove spaces
344 $version = str_replace(' ', '', $version);
345
346 // correct special version strings
347 $version = str_ireplace('dev', 'dev', $version);
348 $version = str_ireplace('alpha', 'alpha', $version);
349 $version = str_ireplace('beta', 'beta', $version);
350 $version = str_ireplace('RC', 'RC', $version);
351 $version = str_ireplace('pl', 'pl', $version);
352
353 return $version;
354 }
355
11ade432 356 /**
aac1247e 357 * Writes the config.inc.php for an application.
9f959ced 358 *
11ade432
AE
359 * @param integer $packageID
360 */
361 public static function writeConfigFile($packageID) {
362 $package = new Package($packageID);
363 $packageDir = FileUtil::addTrailingSlash(FileUtil::getRealPath(WCF_DIR.$package->packageDir));
b842faa1 364
e421b813 365 $prefix = strtoupper(self::getAbbreviation($package->package));
11ade432 366
b842faa1
AE
367 $content = "<?php\n";
368 $content .= "// {$package->package} (packageID {$packageID})\n";
369 $content .= "if (!defined('{$prefix}_DIR')) define('{$prefix}_DIR', __DIR__.'/');\n";
370 $content .= "if (!defined('PACKAGE_ID')) define('PACKAGE_ID', {$packageID});\n";
371 $content .= "if (!defined('PACKAGE_NAME')) define('PACKAGE_NAME', '" . addcslashes($package->getName(), "'") . "');\n";
372 $content .= "if (!defined('PACKAGE_VERSION')) define('PACKAGE_VERSION', '{$package->packageVersion}');\n";
373
374 if ($packageID != 1) {
375 $content .= "\n";
376 $content .= "// helper constants for applications\n";
377 $content .= "if (!defined('RELATIVE_{$prefix}_DIR')) define('RELATIVE_{$prefix}_DIR', '');\n";
378 $content .= "if (!defined('RELATIVE_WCF_DIR')) define('RELATIVE_WCF_DIR', RELATIVE_{$prefix}_DIR.'" . FileUtil::getRelativePath($packageDir, WCF_DIR) . "');\n";
379 }
11ade432 380
b842faa1 381 file_put_contents($packageDir . PackageInstallationDispatcher::CONFIG_FILE, $content);
11ade432 382
9353ac84
AE
383 // add legacy config.inc.php file for backwards compatibility
384 if ($packageID != 1) {
385 // force overwriting the `config.inc.php` unless it is the core itself
5ce29b55 386 file_put_contents($packageDir.'config.inc.php', "<?php" . "\n" . "require_once(__DIR__ . '/".PackageInstallationDispatcher::CONFIG_FILE."');\n");
b842faa1 387 }
11ade432 388 }
11ade432 389}