Merge branch '2.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / validation / PackageValidationArchive.class.php
1 <?php
2 namespace wcf\system\package\validation;
3 use wcf\data\package\Package;
4 use wcf\data\package\PackageCache;
5 use wcf\system\package\PackageArchive;
6
7 /**
8 * Recursively validates the package archive and it's delivered requirements.
9 *
10 * @author Alexander Ebert
11 * @copyright 2001-2014 WoltLab GmbH
12 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
13 * @package com.woltlab.wcf
14 * @subpackage system.package.validation
15 * @category Community Framework
16 */
17 class PackageValidationArchive implements \RecursiveIterator {
18 /**
19 * package archive object
20 * @var \wcf\system\package\PackageArchive
21 */
22 protected $archive = null;
23
24 /**
25 * list of direct requirements delivered by this package
26 * @var array<\wcf\system\package\validation\PackageValidationArchive>
27 */
28 protected $children = array();
29
30 /**
31 * nesting depth
32 * @var integer
33 */
34 protected $depth = 0;
35
36 /**
37 * exception occured during validation
38 * @var \Exception
39 */
40 protected $exception = null;
41
42 /**
43 * associated package object
44 * @var \wcf\data\package\Package
45 */
46 protected $package = null;
47
48 /**
49 * parent package validation archive object
50 * @var \wcf\system\package\validation\PackageValidationArchive
51 */
52 protected $parent = null;
53
54 /**
55 * children pointer
56 * @var integer
57 */
58 private $position = 0;
59
60 /**
61 * Creates a new package validation archive instance.
62 *
63 * @param string $archive
64 * @param \wcf\system\package\validation\PackageValidationArchive $parent
65 * @param integer $depth
66 */
67 public function __construct($archive, PackageValidationArchive $parent = null, $depth = 0) {
68 $this->archive = new PackageArchive($archive);
69 $this->parent = $parent;
70 $this->depth = $depth;
71 }
72
73 /**
74 * Validates this package and optionally it's delivered requirements. Unless you turn on
75 * $deepInspection, this will only check if the archive is theoretically usable to install
76 * or update. This means that neither exclusions nor dependencies will be checked.
77 *
78 * @param boolean $deepInspection
79 * @return boolean
80 */
81 public function validate($deepInspection, $requiredVersion = '') {
82 try {
83 // try to read archive
84 $this->archive->openArchive();
85
86 // check if package is installable or suitable for an update
87 $this->validateInstructions($requiredVersion, $deepInspection);
88 }
89 catch (\Exception $e) {
90 $this->exception = $e;
91
92 return false;
93 }
94
95 if ($deepInspection) {
96 try {
97 PackageValidationManager::getInstance()->addVirtualPackage($this->archive->getPackageInfo('name'), $this->archive->getPackageInfo('version'));
98
99 // check for exclusions
100 // TODO: exclusions are not checked for testing purposes
101 // REMOVE THIS BEFORE *ANY* PUBLIC RELEASE
102 if (WCF_VERSION != '2.1.0 Alpha 1 (Typhoon)') {
103 $this->validateExclusion();
104 }
105
106 // traverse open requirements
107 foreach ($this->archive->getOpenRequirements() as $requirement) {
108 $virtualPackageVersion = PackageValidationManager::getInstance()->getVirtualPackage($requirement['name']);
109 if ($virtualPackageVersion === null || Package::compareVersion($virtualPackageVersion, $requirement['minversion'], '<')) {
110 if (empty($requirement['file'])) {
111 throw new PackageValidationException(PackageValidationException::MISSING_REQUIREMENT, array(
112 'packageName' => $requirement['name'],
113 'packageVersion' => $requirement['minversion']
114 ));
115 }
116
117 $archive = $this->archive->extractTar($requirement['file']);
118
119 $index = count($this->children);
120 $this->children[$index] = new PackageValidationArchive($archive, $this, $this->depth + 1);
121 if (!$this->children[$index]->validate(true, $requirement['minversion'])) {
122 return false;
123 }
124
125 PackageValidationManager::getInstance()->addVirtualPackage(
126 $this->children[$index]->getArchive()->getPackageInfo('name'),
127 $this->children[$index]->getArchive()->getPackageInfo('version')
128 );
129 }
130 }
131 }
132 catch (PackageValidationException $e) {
133 $this->exception = $e;
134
135 return false;
136 }
137 }
138
139 return true;
140
141 }
142
143 /**
144 * Validates if the package has suitable install or update instructions. Setting $deepInspection
145 * to true will cause every single instruction to be validated against the corresponding PIP.
146 *
147 * Please be aware that unknown PIPs will be silently ignored and will not cause any error!
148 *
149 * @param string $requiredVersion
150 * @param boolean $deepInspection
151 */
152 protected function validateInstructions($requiredVersion, $deepInspection) {
153 $package = $this->getPackage();
154
155 // delivered package does not provide the minimum required version
156 if (Package::compareVersion($requiredVersion, $this->archive->getPackageInfo('version'), '>')) {
157 throw new PackageValidationException(PackageValidationException::INSUFFICIENT_VERSION, array(
158 'packageName' => $package->packageName,
159 'packageVersion' => $package->packageVersion,
160 'deliveredPackageVersion' => $this->archive->getPackageInfo('version')
161 ));
162 }
163
164 // package is not installed yet
165 if ($package === null) {
166 $instructions = $this->archive->getInstallInstructions();
167 if (empty($instructions)) {
168 throw new PackageValidationException(PackageValidationException::NO_INSTALL_PATH, array('packageName' => $this->archive->getPackageInfo('name')));
169 }
170
171 if ($deepInspection) {
172 $this->validatePackageInstallationPlugins('install', $instructions);
173 }
174 }
175 else {
176 // package is already installed, check update path
177 if (!$this->archive->isValidUpdate($package)) {
178 throw new PackageValidationException(PackageValidationException::NO_UPDATE_PATH, array(
179 'packageName' => $package->packageName,
180 'packageVersion' => $package->packageVersion,
181 'deliveredPackageVersion' => $this->archive->getPackageInfo('version')
182 ));
183 }
184
185 if ($deepInspection) {
186 $this->validatePackageInstallationPlugins('update', $this->archive->getUpdateInstructions());
187 }
188 }
189 }
190
191 /**
192 * Validates install or update instructions against the corresponding PIP, unknown PIPs will be silently ignored.
193 *
194 * @param string $type
195 * @param array<array> $instructions
196 */
197 protected function validatePackageInstallationPlugins($type, array $instructions) {
198 for ($i = 0, $length = count($instructions); $i < $length; $i++) {
199 $instruction = $instructions[$i];
200 if (!PackageValidationManager::getInstance()->validatePackageInstallationPluginInstruction($this->archive, $instruction['pip'], $instruction['value'])) {
201 throw new PackageValidationException(PackageValidationException::MISSING_INSTRUCTION_FILE, array(
202 'pip' => $instruction['pip'],
203 'type' => $type,
204 'value' => $instruction['value']
205 ));
206 }
207 }
208 }
209
210 /**
211 * Validates if an installed package excludes the current package and vice versa.
212 */
213 protected function validateExclusion() {
214 $excludingPackages = $this->archive->getConflictedExcludingPackages();
215 if (!empty($excludingPackages)) {
216 throw new PackageValidationException(PackageValidationException::EXCLUDING_PACKAGES, array('packages' => $excludingPackages));
217 }
218
219 $excludedPackages = $this->archive->getConflictedExcludedPackages();
220 if (!empty($excludedPackages)) {
221 throw new PackageValidationException(PackageValidationException::EXCLUDED_PACKAGES, array('packages' => $excludedPackages));
222 }
223 }
224
225 /**
226 * Returns the exception message.
227 *
228 * @return string
229 */
230 public function getExceptionMessage() {
231 if ($this->exception === null) {
232 return '';
233 }
234
235 if ($this->exception instanceof PackageValidationException) {
236 return $this->exception->getErrorMessage();
237 }
238
239 return $this->exception->getMessage();
240 }
241
242 /**
243 * Returns the package archive object.
244 *
245 * @return \wcf\system\package\PackageArchive
246 */
247 public function getArchive() {
248 return $this->archive;
249 }
250
251 /**
252 * Returns the package object based on the package archive's package identifier or null
253 * if the package isn't already installed.
254 *
255 * @return \wcf\data\package\Package
256 */
257 public function getPackage() {
258 if ($this->package === null) {
259 $this->package = PackageCache::getInstance()->getPackageByIdentifier($this->archive->getPackageInfo('name'));
260 }
261
262 return $this->package;
263 }
264
265 /**
266 * Returns nesting depth.
267 *
268 * @return integer
269 */
270 public function getDepth() {
271 return $this->depth;
272 }
273
274 /**
275 * Sets the children of this package validation archive.
276 *
277 * @param array<\wcf\system\package\validation\PackageValidationArchive> $children
278 */
279 public function setChildren(array $children) {
280 $this->children = $children;
281 }
282
283 /**
284 * @see \Iterator::rewind()
285 */
286 public function rewind() {
287 $this->position = 0;
288 }
289
290 /**
291 * @see \Iterator::valid()
292 */
293 public function valid() {
294 return isset($this->children[$this->position]);
295 }
296
297 /**
298 * @see \Iterator::next()
299 */
300 public function next() {
301 $this->position++;
302 }
303
304 /**
305 * @see \Iterator::current()
306 */
307 public function current() {
308 return $this->children[$this->position];
309 }
310
311 /**
312 * @see \Iterator::key()
313 */
314 public function key() {
315 return $this->position;
316 }
317
318 /**
319 * @see \RecursiveIterator::getChildren()
320 */
321 public function getChildren() {
322 return $this->children[$this->position];
323 }
324
325 /**
326 * @see \RecursiveIterator::hasChildren()
327 */
328 public function hasChildren() {
329 return count($this->children) > 0;
330 }
331 }