/**
* Executes moderation queue-related actions.
- *
- * @author Alexander Ebert
- * @copyright 2001-2020 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package WoltLabSuite\Core\Data\Moderation\Queue
- *
- * @method ModerationQueueEditor[] getObjects()
- * @method ModerationQueueEditor getSingleObject()
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2020 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Moderation\Queue
+ *
+ * @method ModerationQueueEditor[] getObjects()
+ * @method ModerationQueueEditor getSingleObject()
*/
-class ModerationQueueAction extends AbstractDatabaseObjectAction {
- /**
- * @inheritDoc
- */
- protected $className = ModerationQueueEditor::class;
-
- /**
- * moderation queue editor object
- * @var ModerationQueueEditor
- */
- public $moderationQueueEditor = null;
-
- /**
- * user object
- * @var User
- */
- public $user = null;
-
- /**
- * @inheritDoc
- * @return ModerationQueue
- */
- public function create() {
- if (!isset($this->parameters['data']['lastChangeTime'])) {
- $this->parameters['data']['lastChangeTime'] = TIME_NOW;
- }
-
- /** @noinspection PhpIncompatibleReturnTypeInspection */
- return parent::create();
- }
-
- /**
- * @inheritDoc
- */
- public function update() {
- if (!isset($this->parameters['data']['lastChangeTime'])) {
- $this->parameters['data']['lastChangeTime'] = TIME_NOW;
- }
-
- parent::update();
- }
-
- /**
- * Marks a list of objects as done.
- */
- public function markAsDone() {
- if (empty($this->objects)) {
- $this->readObjects();
- }
-
- $queueIDs = [];
- foreach ($this->getObjects() as $queue) {
- $queueIDs[] = $queue->queueID;
- }
-
- $conditions = new PreparedStatementConditionBuilder();
- $conditions->add("queueID IN (?)", [$queueIDs]);
-
- $sql = "UPDATE wcf".WCF_N."_moderation_queue
- SET status = ".ModerationQueue::STATUS_DONE."
- ".$conditions;
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute($conditions->getParameters());
-
- // reset number of active moderation queue items
- ModerationQueueManager::getInstance()->resetModerationCount();
- }
-
- /**
- * Validates parameters to fetch a list of outstanding queues.
- */
- public function validateGetOutstandingQueues() {
- WCF::getSession()->checkPermissions(['mod.general.canUseModeration']);
- }
-
- /**
- * Returns a list of outstanding queues.
- *
- * @return string[]
- */
- public function getOutstandingQueues() {
- // Maximum cardinality of the returned array
- static $MAX_ITEMS = 10;
-
- $conditions = new PreparedStatementConditionBuilder();
- $conditions->add("moderation_queue_to_user.userID = ?", [WCF::getUser()->userID]);
- $conditions->add("moderation_queue_to_user.isAffected = ?", [1]);
- $conditions->add("moderation_queue.status IN (?)", [[ModerationQueue::STATUS_OUTSTANDING, ModerationQueue::STATUS_PROCESSING]]);
- $conditions->add("moderation_queue.time > ?", [VisitTracker::getInstance()->getVisitTime('com.woltlab.wcf.moderation.queue')]);
- $conditions->add("(moderation_queue.time > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)");
-
- $sql = "SELECT moderation_queue.queueID
- FROM wcf".WCF_N."_moderation_queue_to_user moderation_queue_to_user
- LEFT JOIN wcf".WCF_N."_moderation_queue moderation_queue
- ON (moderation_queue.queueID = moderation_queue_to_user.queueID)
- LEFT JOIN wcf".WCF_N."_tracked_visit tracked_visit
- ON (tracked_visit.objectTypeID = ".VisitTracker::getInstance()->getObjectTypeID('com.woltlab.wcf.moderation.queue')." AND tracked_visit.objectID = moderation_queue.queueID AND tracked_visit.userID = ".WCF::getUser()->userID.")
- ".$conditions."
- ORDER BY moderation_queue.lastChangeTime DESC";
- $statement = WCF::getDB()->prepareStatement($sql, $MAX_ITEMS);
- $statement->execute($conditions->getParameters());
- $queueIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
-
- $queues = [];
- if (!empty($queueIDs)) {
- $queueList = new ViewableModerationQueueList();
- $queueList->getConditionBuilder()->add("moderation_queue.queueID IN (?)", [$queueIDs]);
- $queueList->sqlOrderBy = "moderation_queue.lastChangeTime DESC";
- $queueList->readObjects();
- foreach ($queueList as $queue) {
- $queues[] = $queue;
- }
- }
-
- // check if user storage is outdated
- $totalCount = ModerationQueueManager::getInstance()->getUnreadModerationCount();
- $count = count($queues);
- if ($count < $MAX_ITEMS) {
- // load more entries to fill up list
- $queueList = new ViewableModerationQueueList();
- $queueList->getConditionBuilder()->add("moderation_queue.status IN (?)", [[ModerationQueue::STATUS_OUTSTANDING, ModerationQueue::STATUS_PROCESSING]]);
- if (!empty($queueIDs)) $queueList->getConditionBuilder()->add("moderation_queue.queueID NOT IN (?)", [$queueIDs]);
- $queueList->sqlOrderBy = "moderation_queue.lastChangeTime DESC";
- $queueList->sqlLimit = $MAX_ITEMS - $count;
- $queueList->readObjects();
- foreach ($queueList as $queue) {
- $queues[] = $queue;
- }
-
- // check if stored count is out of sync
- if ($count < $totalCount) {
- UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'outstandingModerationCount');
-
- // check for orphaned queues
- $queueCount = ModerationQueueManager::getInstance()->getUnreadModerationCount();
- if (count($queues) < $queueCount) {
- ModerationQueueManager::getInstance()->identifyOrphans();
- }
- }
- }
-
- WCF::getTPL()->assign([
- 'queues' => $queues
- ]);
-
- return [
- 'template' => WCF::getTPL()->fetch('moderationQueueList'),
- 'totalCount' => $totalCount
- ];
- }
-
- /**
- * Validates parameters to show the user assign form.
- */
- public function validateGetAssignUserForm() {
- $this->moderationQueueEditor = $this->getSingleObject();
-
- // check if queue is accessible for current user
- if (!$this->moderationQueueEditor->canEdit()) {
- throw new PermissionDeniedException();
- }
- }
-
- /**
- * Returns the user assign form.
- *
- * @return string[]
- */
- public function getAssignUserForm() {
- $assignedUser = $this->moderationQueueEditor->assignedUserID ? new User($this->moderationQueueEditor->assignedUserID) : null;
-
- WCF::getTPL()->assign([
- 'assignedUser' => $assignedUser,
- 'queue' => $this->moderationQueueEditor
- ]);
-
- return [
- 'template' => WCF::getTPL()->fetch('moderationQueueAssignUser')
- ];
- }
-
- /**
- * Validates parameters to assign a user.
- */
- public function validateAssignUser() {
- // To assign a user we need to be able to fetch the assignment form.
- $this->validateGetAssignUserForm();
-
- $this->readInteger('assignedUserID', true);
-
- if ($this->parameters['assignedUserID'] && $this->parameters['assignedUserID'] != -1) {
- if ($this->parameters['assignedUserID'] != WCF::getUser()->userID && $this->parameters['assignedUserID'] != $this->moderationQueueEditor->assignedUserID) {
- // user id is either faked or changed during viewing, use database value instead
- $this->parameters['assignedUserID'] = $this->moderationQueueEditor->assignedUserID;
- }
- }
-
- if ($this->parameters['assignedUserID'] == -1) {
- $this->readString('assignedUsername');
-
- $this->user = User::getUserByUsername($this->parameters['assignedUsername']);
- if (!$this->user->userID) {
- throw new UserInputException('assignedUsername', 'notFound');
- }
-
- // get handler
- $objectType = ObjectTypeCache::getInstance()->getObjectType($this->moderationQueueEditor->objectTypeID);
- if (!$objectType->getProcessor()->isAffectedUser($this->moderationQueueEditor->getDecoratedObject(), $this->user->userID)) {
- throw new UserInputException('assignedUsername', 'notAffected');
- }
-
- $this->parameters['assignedUserID'] = $this->user->userID;
- $this->parameters['assignedUsername'] = '';
- }
- else {
- $this->user = new User($this->parameters['assignedUserID']);
- }
- }
-
- /**
- * Returns the data for the newly assigned user.
- *
- * @return string[]
- */
- public function assignUser() {
- $data = ['assignedUserID' => $this->parameters['assignedUserID'] ?: null];
- if ($this->user->userID) {
- if ($this->moderationQueueEditor->status == ModerationQueue::STATUS_OUTSTANDING) {
- $data['status'] = ModerationQueue::STATUS_PROCESSING;
- }
- }
- else {
- if ($this->moderationQueueEditor->status == ModerationQueue::STATUS_PROCESSING) {
- $data['status'] = ModerationQueue::STATUS_OUTSTANDING;
- }
- }
-
- $this->moderationQueueEditor->update($data);
-
- $username = $this->user->userID ? $this->user->username : WCF::getLanguage()->get('wcf.moderation.assignedUser.nobody');
- $link = '';
- if ($this->user->userID) {
- $link = $this->user->getLink();
- }
-
- $newStatus = '';
- if (isset($data['status'])) {
- $newStatus = ($data['status'] == ModerationQueue::STATUS_OUTSTANDING) ? 'outstanding' : 'processing';
- }
-
- return [
- 'link' => $link,
- 'newStatus' => $newStatus,
- 'userID' => $this->user->userID,
- 'username' => $username
- ];
- }
-
- /**
- * Marks queue entries as read.
- */
- public function markAsRead() {
- if (empty($this->parameters['visitTime'])) {
- $this->parameters['visitTime'] = TIME_NOW;
- }
-
- if (empty($this->objects)) {
- $this->readObjects();
- }
-
- foreach ($this->getObjects() as $queue) {
- VisitTracker::getInstance()->trackObjectVisit('com.woltlab.wcf.moderation.queue', $queue->queueID, $this->parameters['visitTime']);
- }
-
- // reset storage
- UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadModerationCount');
-
- if (count($this->objects) == 1) {
- $queue = reset($this->objects);
-
- return [
- 'markAsRead' => $queue->queueID,
- 'totalCount' => ModerationQueueManager::getInstance()->getUnreadModerationCount(true)
- ];
- }
- }
-
- /**
- * @inheritDoc
- */
- public function validateMarkAsRead() {
- if (empty($this->objects)) {
- $this->readObjects();
- }
-
- foreach ($this->getObjects() as $queue) {
- if (!$queue->canEdit()) {
- throw new PermissionDeniedException();
- }
- }
- }
-
- /**
- * Marks all queue entries as read.
- */
- public function markAllAsRead() {
- VisitTracker::getInstance()->trackTypeVisit('com.woltlab.wcf.moderation.queue');
-
- // reset storage
- UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadModerationCount');
-
- return [
- 'markAllAsRead' => true
- ];
- }
-
- /**
- * Validates the mark all as read action.
- */
- public function validateMarkAllAsRead() {
- // does nothing
- }
+class ModerationQueueAction extends AbstractDatabaseObjectAction
+{
+ /**
+ * @inheritDoc
+ */
+ protected $className = ModerationQueueEditor::class;
+
+ /**
+ * moderation queue editor object
+ * @var ModerationQueueEditor
+ */
+ public $moderationQueueEditor;
+
+ /**
+ * user object
+ * @var User
+ */
+ public $user;
+
+ /**
+ * @inheritDoc
+ * @return ModerationQueue
+ */
+ public function create()
+ {
+ if (!isset($this->parameters['data']['lastChangeTime'])) {
+ $this->parameters['data']['lastChangeTime'] = TIME_NOW;
+ }
+
+ /** @noinspection PhpIncompatibleReturnTypeInspection */
+ return parent::create();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function update()
+ {
+ if (!isset($this->parameters['data']['lastChangeTime'])) {
+ $this->parameters['data']['lastChangeTime'] = TIME_NOW;
+ }
+
+ parent::update();
+ }
+
+ /**
+ * Marks a list of objects as done.
+ */
+ public function markAsDone()
+ {
+ if (empty($this->objects)) {
+ $this->readObjects();
+ }
+
+ $queueIDs = [];
+ foreach ($this->getObjects() as $queue) {
+ $queueIDs[] = $queue->queueID;
+ }
+
+ $conditions = new PreparedStatementConditionBuilder();
+ $conditions->add("queueID IN (?)", [$queueIDs]);
+
+ $sql = "UPDATE wcf" . WCF_N . "_moderation_queue
+ SET status = " . ModerationQueue::STATUS_DONE . "
+ " . $conditions;
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute($conditions->getParameters());
+
+ // reset number of active moderation queue items
+ ModerationQueueManager::getInstance()->resetModerationCount();
+ }
+
+ /**
+ * Validates parameters to fetch a list of outstanding queues.
+ */
+ public function validateGetOutstandingQueues()
+ {
+ WCF::getSession()->checkPermissions(['mod.general.canUseModeration']);
+ }
+
+ /**
+ * Returns a list of outstanding queues.
+ *
+ * @return string[]
+ */
+ public function getOutstandingQueues()
+ {
+ // Maximum cardinality of the returned array
+ static $MAX_ITEMS = 10;
+
+ $conditions = new PreparedStatementConditionBuilder();
+ $conditions->add("moderation_queue_to_user.userID = ?", [WCF::getUser()->userID]);
+ $conditions->add("moderation_queue_to_user.isAffected = ?", [1]);
+ $conditions->add(
+ "moderation_queue.status IN (?)",
+ [[ModerationQueue::STATUS_OUTSTANDING, ModerationQueue::STATUS_PROCESSING]]
+ );
+ $conditions->add(
+ "moderation_queue.time > ?",
+ [VisitTracker::getInstance()->getVisitTime('com.woltlab.wcf.moderation.queue')]
+ );
+ $conditions->add("(moderation_queue.time > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)");
+
+ $sql = "SELECT moderation_queue.queueID
+ FROM wcf" . WCF_N . "_moderation_queue_to_user moderation_queue_to_user
+ LEFT JOIN wcf" . WCF_N . "_moderation_queue moderation_queue
+ ON moderation_queue.queueID = moderation_queue_to_user.queueID
+ LEFT JOIN wcf" . WCF_N . "_tracked_visit tracked_visit
+ ON tracked_visit.objectTypeID = " . VisitTracker::getInstance()->getObjectTypeID('com.woltlab.wcf.moderation.queue') . "
+ AND tracked_visit.objectID = moderation_queue.queueID
+ AND tracked_visit.userID = " . WCF::getUser()->userID . "
+ " . $conditions . "
+ ORDER BY moderation_queue.lastChangeTime DESC";
+ $statement = WCF::getDB()->prepareStatement($sql, $MAX_ITEMS);
+ $statement->execute($conditions->getParameters());
+ $queueIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
+
+ $queues = [];
+ if (!empty($queueIDs)) {
+ $queueList = new ViewableModerationQueueList();
+ $queueList->getConditionBuilder()->add("moderation_queue.queueID IN (?)", [$queueIDs]);
+ $queueList->sqlOrderBy = "moderation_queue.lastChangeTime DESC";
+ $queueList->readObjects();
+ foreach ($queueList as $queue) {
+ $queues[] = $queue;
+ }
+ }
+
+ // check if user storage is outdated
+ $totalCount = ModerationQueueManager::getInstance()->getUnreadModerationCount();
+ $count = \count($queues);
+ if ($count < $MAX_ITEMS) {
+ // load more entries to fill up list
+ $queueList = new ViewableModerationQueueList();
+ $queueList->getConditionBuilder()->add(
+ "moderation_queue.status IN (?)",
+ [[ModerationQueue::STATUS_OUTSTANDING, ModerationQueue::STATUS_PROCESSING]]
+ );
+ if (!empty($queueIDs)) {
+ $queueList->getConditionBuilder()->add("moderation_queue.queueID NOT IN (?)", [$queueIDs]);
+ }
+ $queueList->sqlOrderBy = "moderation_queue.lastChangeTime DESC";
+ $queueList->sqlLimit = $MAX_ITEMS - $count;
+ $queueList->readObjects();
+ foreach ($queueList as $queue) {
+ $queues[] = $queue;
+ }
+
+ // check if stored count is out of sync
+ if ($count < $totalCount) {
+ UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'outstandingModerationCount');
+
+ // check for orphaned queues
+ $queueCount = ModerationQueueManager::getInstance()->getUnreadModerationCount();
+ if (\count($queues) < $queueCount) {
+ ModerationQueueManager::getInstance()->identifyOrphans();
+ }
+ }
+ }
+
+ WCF::getTPL()->assign([
+ 'queues' => $queues,
+ ]);
+
+ return [
+ 'template' => WCF::getTPL()->fetch('moderationQueueList'),
+ 'totalCount' => $totalCount,
+ ];
+ }
+
+ /**
+ * Validates parameters to show the user assign form.
+ */
+ public function validateGetAssignUserForm()
+ {
+ $this->moderationQueueEditor = $this->getSingleObject();
+
+ // check if queue is accessible for current user
+ if (!$this->moderationQueueEditor->canEdit()) {
+ throw new PermissionDeniedException();
+ }
+ }
+
+ /**
+ * Returns the user assign form.
+ *
+ * @return string[]
+ */
+ public function getAssignUserForm()
+ {
+ $assignedUser = $this->moderationQueueEditor->assignedUserID ? new User($this->moderationQueueEditor->assignedUserID) : null;
+
+ WCF::getTPL()->assign([
+ 'assignedUser' => $assignedUser,
+ 'queue' => $this->moderationQueueEditor,
+ ]);
+
+ return [
+ 'template' => WCF::getTPL()->fetch('moderationQueueAssignUser'),
+ ];
+ }
+
+ /**
+ * Validates parameters to assign a user.
+ */
+ public function validateAssignUser()
+ {
- $this->moderationQueueEditor = $this->getSingleObject();
++ // To assign a user we need to be able to fetch the assignment form.
++ $this->validateGetAssignUserForm();
++
+ $this->readInteger('assignedUserID', true);
+
+ if ($this->parameters['assignedUserID'] && $this->parameters['assignedUserID'] != -1) {
+ if ($this->parameters['assignedUserID'] != WCF::getUser()->userID && $this->parameters['assignedUserID'] != $this->moderationQueueEditor->assignedUserID) {
+ // user id is either faked or changed during viewing, use database value instead
+ $this->parameters['assignedUserID'] = $this->moderationQueueEditor->assignedUserID;
+ }
+ }
+
+ if ($this->parameters['assignedUserID'] == -1) {
+ $this->readString('assignedUsername');
+
+ $this->user = User::getUserByUsername($this->parameters['assignedUsername']);
+ if (!$this->user->userID) {
+ throw new UserInputException('assignedUsername', 'notFound');
+ }
+
+ // get handler
+ $objectType = ObjectTypeCache::getInstance()->getObjectType($this->moderationQueueEditor->objectTypeID);
+ if (
+ !$objectType->getProcessor()->isAffectedUser(
+ $this->moderationQueueEditor->getDecoratedObject(),
+ $this->user->userID
+ )
+ ) {
+ throw new UserInputException('assignedUsername', 'notAffected');
+ }
+
+ $this->parameters['assignedUserID'] = $this->user->userID;
+ $this->parameters['assignedUsername'] = '';
+ } else {
+ $this->user = new User($this->parameters['assignedUserID']);
+ }
+ }
+
+ /**
+ * Returns the data for the newly assigned user.
+ *
+ * @return string[]
+ */
+ public function assignUser()
+ {
+ $data = ['assignedUserID' => $this->parameters['assignedUserID'] ?: null];
+ if ($this->user->userID) {
+ if ($this->moderationQueueEditor->status == ModerationQueue::STATUS_OUTSTANDING) {
+ $data['status'] = ModerationQueue::STATUS_PROCESSING;
+ }
+ } else {
+ if ($this->moderationQueueEditor->status == ModerationQueue::STATUS_PROCESSING) {
+ $data['status'] = ModerationQueue::STATUS_OUTSTANDING;
+ }
+ }
+
+ $this->moderationQueueEditor->update($data);
+
+ $username = $this->user->userID ? $this->user->username : WCF::getLanguage()->get('wcf.moderation.assignedUser.nobody');
+ $link = '';
+ if ($this->user->userID) {
+ $link = $this->user->getLink();
+ }
+
+ $newStatus = '';
+ if (isset($data['status'])) {
+ $newStatus = ($data['status'] == ModerationQueue::STATUS_OUTSTANDING) ? 'outstanding' : 'processing';
+ }
+
+ return [
+ 'link' => $link,
+ 'newStatus' => $newStatus,
+ 'userID' => $this->user->userID,
+ 'username' => $username,
+ ];
+ }
+
+ /**
+ * Marks queue entries as read.
+ */
+ public function markAsRead()
+ {
+ if (empty($this->parameters['visitTime'])) {
+ $this->parameters['visitTime'] = TIME_NOW;
+ }
+
+ if (empty($this->objects)) {
+ $this->readObjects();
+ }
+
+ foreach ($this->getObjects() as $queue) {
+ VisitTracker::getInstance()->trackObjectVisit(
+ 'com.woltlab.wcf.moderation.queue',
+ $queue->queueID,
+ $this->parameters['visitTime']
+ );
+ }
+
+ // reset storage
+ UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadModerationCount');
+
+ if (\count($this->objects) == 1) {
+ $queue = \reset($this->objects);
+
+ return [
+ 'markAsRead' => $queue->queueID,
+ 'totalCount' => ModerationQueueManager::getInstance()->getUnreadModerationCount(true),
+ ];
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validateMarkAsRead()
+ {
+ if (empty($this->objects)) {
+ $this->readObjects();
+ }
+
+ foreach ($this->getObjects() as $queue) {
+ if (!$queue->canEdit()) {
+ throw new PermissionDeniedException();
+ }
+ }
+ }
+
+ /**
+ * Marks all queue entries as read.
+ */
+ public function markAllAsRead()
+ {
+ VisitTracker::getInstance()->trackTypeVisit('com.woltlab.wcf.moderation.queue');
+
+ // reset storage
+ UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadModerationCount');
+
+ return [
+ 'markAllAsRead' => true,
+ ];
+ }
+
+ /**
+ * Validates the mark all as read action.
+ */
+ public function validateMarkAllAsRead()
+ {
+ // does nothing
+ }
}
/**
* PackageInstallationDispatcher handles the whole installation process.
- *
- * @author Alexander Ebert
- * @copyright 2001-2019 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package WoltLabSuite\Core\System\Package
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Package
*/
-class PackageInstallationDispatcher {
- /**
- * current installation type
- * @var string
- */
- protected $action = '';
-
- /**
- * instance of PackageArchive
- * @var PackageArchive
- */
- public $archive;
-
- /**
- * instance of PackageInstallationNodeBuilder
- * @var PackageInstallationNodeBuilder
- */
- public $nodeBuilder;
-
- /**
- * instance of Package
- * @var Package
- */
- public $package;
-
- /**
- * instance of PackageInstallationQueue
- * @var PackageInstallationQueue
- */
- public $queue;
-
- /**
- * default name of the config file
- * @var string
- */
- const CONFIG_FILE = 'app.config.inc.php';
-
- /**
- * data of previous package in queue
- * @var string[]
- */
- protected $previousPackageData;
-
- /**
- * Creates a new instance of PackageInstallationDispatcher.
- *
- * @param PackageInstallationQueue $queue
- */
- public function __construct(PackageInstallationQueue $queue) {
- $this->queue = $queue;
- $this->nodeBuilder = new PackageInstallationNodeBuilder($this);
-
- $this->action = $this->queue->action;
- }
-
- /**
- * Sets data of previous package in queue.
- *
- * @param string[] $packageData
- */
- public function setPreviousPackage(array $packageData) {
- $this->previousPackageData = $packageData;
- }
-
- /**
- * Installs node components and returns next node.
- *
- * @param string $node
- * @return PackageInstallationStep
- * @throws SystemException
- */
- public function install($node) {
- $nodes = $this->nodeBuilder->getNodeData($node);
- if (empty($nodes)) {
- // guard against possible issues with empty instruction blocks, including
- // these blocks that contain no valid instructions at all (e.g. typo from
- // copy & paste)
- throw new SystemException("Failed to retrieve nodes for identifier '".$node."', the query returned no results.");
- }
-
- // invoke node-specific actions
- $step = null;
- foreach ($nodes as $data) {
- $nodeData = unserialize($data['nodeData']);
- $this->logInstallationStep($data);
-
- switch ($data['nodeType']) {
- case 'package':
- $step = $this->installPackage($nodeData);
- break;
-
- case 'pip':
- $step = $this->executePIP($nodeData);
- break;
-
- case 'optionalPackages':
- $step = $this->selectOptionalPackages($node, $nodeData);
- break;
-
- default:
- die("Unknown node type: '".$data['nodeType']."'");
- break;
- }
-
- if ($step->splitNode()) {
- $log = 'split node';
- if ($step->getException() !== null && $step->getException()->getMessage()) {
- $log .= ': ' . $step->getException()->getMessage();
- }
-
- $this->logInstallationStep($data, $log);
- $this->nodeBuilder->cloneNode($node, $data['sequenceNo']);
- break;
- }
- }
-
- // mark node as completed
- $this->nodeBuilder->completeNode($node);
-
- // assign next node
- $node = $this->nodeBuilder->getNextNode($node);
- $step->setNode($node);
-
- // perform post-install/update actions
- if ($node == '') {
- $this->logInstallationStep([], 'start cleanup');
-
- // update "last update time" option
- $sql = "UPDATE wcf".WCF_N."_option
- SET optionValue = ?
- WHERE optionName = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([
- TIME_NOW,
- 'last_update_time'
- ]);
-
- // update options.inc.php
- OptionEditor::resetCache();
-
- if ($this->action == 'install') {
- // save localized package infos
- $this->saveLocalizedPackageInfos();
-
- // remove all cache files after WCFSetup
- if (!PACKAGE_ID) {
- CacheHandler::getInstance()->flushAll();
-
- $sql = "UPDATE wcf".WCF_N."_option
- SET optionValue = ?
- WHERE optionName = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
-
- $statement->execute([
- StringUtil::getUUID(),
- 'wcf_uuid'
- ]);
-
- if (file_exists(WCF_DIR . 'cookiePrefix.txt')) {
- $statement->execute([
- COOKIE_PREFIX,
- 'cookie_prefix'
- ]);
-
- @unlink(WCF_DIR . 'cookiePrefix.txt');
- }
-
- $user = new User(1);
- $statement->execute([
- $user->username,
- 'mail_from_name'
- ]);
- $statement->execute([
- $user->email,
- 'mail_from_address'
- ]);
- $statement->execute([
- $user->email,
- 'mail_admin_address'
- ]);
-
- try {
- $statement->execute([
- bin2hex(\random_bytes(20)),
- 'signature_secret'
- ]);
- }
- catch (\Throwable $e) {
- // ignore, the secret will stay empty and crypto operations
- // depending on it will fail
- }
-
- if (WCF::getSession()->getVar('__wcfSetup_developerMode')) {
- $statement->execute([
- 1,
- 'enable_debug_mode'
- ]);
- $statement->execute([
- 'public',
- 'exception_privacy'
- ]);
- $statement->execute([
- 'debugFolder',
- 'mail_send_method'
- ]);
- $statement->execute([
- 1,
- 'enable_developer_tools'
- ]);
- $statement->execute([
- 1,
- 'log_missing_language_items'
- ]);
-
- foreach (DevtoolsSetup::getInstance()->getOptionOverrides() as $optionName => $optionValue) {
- $statement->execute([
- $optionValue,
- $optionName
- ]);
- }
-
- foreach (DevtoolsSetup::getInstance()->getUsers() as $newUser) {
- try {
- (new UserAction([], 'create', [
- 'data' => [
- 'email' => $newUser['email'],
- 'password' => $newUser['password'],
- 'username' => $newUser['username']
- ],
- 'groups' => [
- 1,
- 3
- ]
- ]))->executeAction();
- }
- catch (SystemException $e) {
- // ignore errors due to event listeners missing at this
- // point during installation
- }
- }
-
- if (($importPath = DevtoolsSetup::getInstance()->getDevtoolsImportPath()) !== '') {
- (new DevtoolsProjectAction([], 'quickSetup', [
- 'path' => $importPath
- ]))->executeAction();
- }
- }
-
- if (WCF::getSession()->getVar('__wcfSetup_imagick')) {
- $statement->execute([
- 'imagick',
- 'image_adapter_type',
- ]);
- }
-
- // update options.inc.php
- OptionEditor::resetCache();
-
- WCF::getSession()->register('__wcfSetup_completed', true);
- }
-
- // rebuild application paths
- ApplicationHandler::rebuild();
- }
-
- // remove template listener cache
- TemplateListenerCodeCacheBuilder::getInstance()->reset();
-
- // reset language cache
- LanguageFactory::getInstance()->clearCache();
- LanguageFactory::getInstance()->deleteLanguageCache();
-
- // reset stylesheets
- StyleHandler::resetStylesheets();
-
- // clear user storage
- UserStorageHandler::getInstance()->clear();
-
- // rebuild config files for affected applications
- $sql = "SELECT package.packageID
- FROM wcf".WCF_N."_package_installation_queue queue,
- wcf".WCF_N."_package package
- WHERE queue.processNo = ?
- AND package.packageID = queue.packageID
- AND package.isApplication = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([
- $this->queue->processNo,
- 1
- ]);
- while ($row = $statement->fetchArray()) {
- Package::writeConfigFile($row['packageID']);
- }
-
- EventHandler::getInstance()->fireAction($this, 'postInstall');
-
- // remove archives
- $sql = "SELECT archive
- FROM wcf".WCF_N."_package_installation_queue
- WHERE processNo = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([$this->queue->processNo]);
- while ($row = $statement->fetchArray()) {
- @unlink($row['archive']);
- }
-
- // delete queues
- $sql = "DELETE FROM wcf".WCF_N."_package_installation_queue
- WHERE processNo = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([$this->queue->processNo]);
-
- $this->logInstallationStep([], 'finished cleanup');
- }
-
- return $step;
- }
-
- /**
- * Logs an installation step.
- *
- * @param array $node data of the executed node
- * @param string $log optional additional log text
- */
- protected function logInstallationStep(array $node = [], $log = '') {
- $logEntry = "[" . TIME_NOW . "]\n";
- if (!empty($node)) {
- $logEntry .= 'sequenceNo: ' . $node['sequenceNo'] . "\n";
- $logEntry .= 'nodeType: ' . $node['nodeType'] . "\n";
- $logEntry .= "nodeData:\n";
-
- $nodeData = unserialize($node['nodeData']);
- foreach ($nodeData as $index => $value) {
- $logEntry .= "\t" . $index . ': ' . (!is_object($value) && !is_array($value) ? $value : JSON::encode($value)) . "\n";
- }
- }
-
- if ($log) {
- $logEntry .= 'additional information: ' . $log . "\n";
- }
-
- $logEntry .= str_repeat('-', 30) . "\n\n";
-
- file_put_contents(
- WCF_DIR . 'log/' . date('Y-m-d', TIME_NOW) . '-update-' . $this->queue->queueID . '.txt',
- $logEntry,
- FILE_APPEND
- );
- }
-
- /**
- * Returns current package archive.
- *
- * @return PackageArchive
- */
- public function getArchive() {
- if ($this->archive === null) {
- // check if we're doing an iterative update of the same package
- if ($this->previousPackageData !== null && $this->getPackage()->package == $this->previousPackageData['package']) {
- if (Package::compareVersion($this->getPackage()->packageVersion, $this->previousPackageData['packageVersion'], '<')) {
- // fake package to simulate the package version required by current archive
- $this->getPackage()->setPackageVersion($this->previousPackageData['packageVersion']);
- }
- }
-
- $this->archive = new PackageArchive($this->queue->archive, $this->getPackage());
-
- if (FileUtil::isURL($this->archive->getArchive())) {
- // get return value and update entry in
- // package_installation_queue with this value
- $archive = $this->archive->downloadArchive();
- $queueEditor = new PackageInstallationQueueEditor($this->queue);
- $queueEditor->update(['archive' => $archive]);
- }
-
- $this->archive->openArchive();
- }
-
- return $this->archive;
- }
-
- /**
- * Installs current package.
- *
- * @param mixed[] $nodeData
- * @return PackageInstallationStep
- * @throws SystemException
- */
- protected function installPackage(array $nodeData) {
- $installationStep = new PackageInstallationStep();
-
- // check requirements
- if (!empty($nodeData['requirements'])) {
- foreach ($nodeData['requirements'] as $package => $requirementData) {
- // get existing package
- if ($requirementData['packageID']) {
- $sql = "SELECT packageName, packageVersion
- FROM wcf".WCF_N."_package
- WHERE packageID = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([$requirementData['packageID']]);
- }
- else {
- // try to find matching package
- $sql = "SELECT packageName, packageVersion
- FROM wcf".WCF_N."_package
- WHERE package = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([$package]);
- }
- $row = $statement->fetchArray();
-
- // package is required but not available
- if ($row === false) {
- throw new SystemException("Package '".$package."' is required by '".$nodeData['packageName']."', but is neither installed nor shipped.");
- }
-
- // check version requirements
- if ($requirementData['minVersion']) {
- if (Package::compareVersion($row['packageVersion'], $requirementData['minVersion']) < 0) {
- throw new SystemException("Package '".$nodeData['packageName']."' requires package '".$row['packageName']."' in version '".$requirementData['minVersion']."', but only version '".$row['packageVersion']."' is installed");
- }
- }
- }
- }
- unset($nodeData['requirements']);
-
- $applicationDirectory = '';
- if (isset($nodeData['applicationDirectory'])) {
- $applicationDirectory = $nodeData['applicationDirectory'];
- unset($nodeData['applicationDirectory']);
- }
-
- // update package
- if ($this->queue->packageID) {
- $packageEditor = new PackageEditor(new Package($this->queue->packageID));
- unset($nodeData['installDate']);
- $packageEditor->update($nodeData);
-
- // delete old excluded packages
- $sql = "DELETE FROM wcf".WCF_N."_package_exclusion
- WHERE packageID = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([$this->queue->packageID]);
-
- // delete old compatibility versions
- $sql = "DELETE FROM wcf".WCF_N."_package_compatibility
- WHERE packageID = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([$this->queue->packageID]);
-
- // delete old requirements and dependencies
- $sql = "DELETE FROM wcf".WCF_N."_package_requirement
- WHERE packageID = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([$this->queue->packageID]);
- }
- else {
- // create package entry
- $package = $this->createPackage($nodeData);
-
- // update package id for current queue
- $queueEditor = new PackageInstallationQueueEditor($this->queue);
- $queueEditor->update(['packageID' => $package->packageID]);
-
- // reload queue
- $this->queue = new PackageInstallationQueue($this->queue->queueID);
- $this->package = null;
-
- if ($package->isApplication) {
- $host = str_replace(RouteHandler::getProtocol(), '', RouteHandler::getHost());
- $path = RouteHandler::getPath(['acp']);
-
- // insert as application
- ApplicationEditor::create([
- 'domainName' => $host,
- 'domainPath' => $path,
- 'cookieDomain' => $host,
- 'packageID' => $package->packageID,
- 'isTainted' => 1,
- ]);
- }
- }
-
- // save excluded packages
- if (count($this->getArchive()->getExcludedPackages())) {
- $sql = "INSERT INTO wcf".WCF_N."_package_exclusion
- (packageID, excludedPackage, excludedPackageVersion)
- VALUES (?, ?, ?)";
- $statement = WCF::getDB()->prepareStatement($sql);
-
- foreach ($this->getArchive()->getExcludedPackages() as $excludedPackage) {
- $statement->execute([
- $this->queue->packageID,
- $excludedPackage['name'],
- !empty($excludedPackage['version']) ? $excludedPackage['version'] : ''
- ]);
- }
- }
-
- // save compatible versions
- if (!empty($this->getArchive()->getCompatibleVersions())) {
- $sql = "INSERT INTO wcf".WCF_N."_package_compatibility
- (packageID, version)
- VALUES (?, ?)";
- $statement = WCF::getDB()->prepareStatement($sql);
-
- foreach ($this->getArchive()->getCompatibleVersions() as $version) {
- $statement->execute([
- $this->queue->packageID,
- $version
- ]);
- }
- }
-
- // insert requirements and dependencies
- $requirements = $this->getArchive()->getAllExistingRequirements();
- if (!empty($requirements)) {
- $sql = "INSERT INTO wcf".WCF_N."_package_requirement
- (packageID, requirement)
- VALUES (?, ?)";
- $statement = WCF::getDB()->prepareStatement($sql);
-
- foreach ($requirements as $identifier => $possibleRequirements) {
- $requirement = array_shift($possibleRequirements);
-
- $statement->execute([
- $this->queue->packageID,
- $requirement['packageID']
- ]);
- }
- }
-
- if ($this->getPackage()->isApplication && $this->getPackage()->package != 'com.woltlab.wcf' && $this->getAction() == 'install' && empty($this->getPackage()->packageDir)) {
- $document = $this->promptPackageDir($applicationDirectory);
- if ($document !== null && $document instanceof FormDocument) {
- $installationStep->setDocument($document);
- }
-
- $installationStep->setSplitNode();
- }
-
- return $installationStep;
- }
-
- /**
- * Creates a new package based on the given data and returns it.
- *
- * @param array $packageData
- * @return Package
- * @since 5.2
- */
- protected function createPackage(array $packageData) {
- return PackageEditor::create($packageData);
- }
-
- /**
- * Saves the localized package info.
- */
- protected function saveLocalizedPackageInfos() {
- $package = new Package($this->queue->packageID);
-
- // localize package information
- $sql = "INSERT INTO wcf".WCF_N."_language_item
- (languageID, languageItem, languageItemValue, languageCategoryID, packageID)
- VALUES (?, ?, ?, ?, ?)";
- $statement = WCF::getDB()->prepareStatement($sql);
-
- // get language list
- $languageList = new LanguageList();
- $languageList->readObjects();
-
- // workaround for WCFSetup
- if (!PACKAGE_ID) {
- $sql = "SELECT *
- FROM wcf".WCF_N."_language_category
- WHERE languageCategory = ?";
- $statement2 = WCF::getDB()->prepareStatement($sql);
- $statement2->execute(['wcf.acp.package']);
- $languageCategory = $statement2->fetchObject(LanguageCategory::class);
- }
- else {
- $languageCategory = LanguageFactory::getInstance()->getCategory('wcf.acp.package');
- }
-
- // save package name
- $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageName');
-
- // save package description
- $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageDescription');
-
- // update description and name
- $packageEditor = new PackageEditor($package);
- $packageEditor->update([
- 'packageDescription' => 'wcf.acp.package.packageDescription.package'.$this->queue->packageID,
- 'packageName' => 'wcf.acp.package.packageName.package'.$this->queue->packageID
- ]);
- }
-
- /**
- * Saves a localized package info.
- *
- * @param PreparedStatement $statement
- * @param LanguageList $languageList
- * @param LanguageCategory $languageCategory
- * @param Package $package
- * @param string $infoName
- */
- protected function saveLocalizedPackageInfo(PreparedStatement $statement, $languageList, LanguageCategory $languageCategory, Package $package, $infoName) {
- $infoValues = $this->getArchive()->getPackageInfo($infoName);
-
- // get default value for languages without specified information
- $defaultValue = '';
- if (isset($infoValues['default'])) {
- $defaultValue = $infoValues['default'];
- }
- else if (isset($infoValues['en'])) {
- // fallback to English
- $defaultValue = $infoValues['en'];
- }
- else if (isset($infoValues[WCF::getLanguage()->getFixedLanguageCode()])) {
- // fallback to the language of the current user
- $defaultValue = $infoValues[WCF::getLanguage()->getFixedLanguageCode()];
- }
- else if ($infoName == 'packageName') {
- // fallback to the package identifier for the package name
- $defaultValue = $this->getArchive()->getPackageInfo('name');
- }
-
- foreach ($languageList as $language) {
- $value = $defaultValue;
- if (isset($infoValues[$language->languageCode])) {
- $value = $infoValues[$language->languageCode];
- }
-
- $statement->execute([
- $language->languageID,
- 'wcf.acp.package.'.$infoName.'.package'.$package->packageID,
- $value,
- $languageCategory->languageCategoryID,
- 1
- ]);
- }
- }
-
- /**
- * Executes a package installation plugin.
- *
- * @param mixed[] $nodeData
- * @return PackageInstallationStep
- * @throws SystemException
- */
- protected function executePIP(array $nodeData) {
- $step = new PackageInstallationStep();
-
- if ($nodeData['pip'] == PackageArchive::VOID_MARKER) {
- return $step;
- }
-
- // fetch all pips associated with current PACKAGE_ID and include pips
- // previously installed by current installation queue
- $sql = "SELECT pluginName, className
- FROM wcf".WCF_N."_package_installation_plugin
- WHERE pluginName = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([$nodeData['pip']]);
- $row = $statement->fetchArray();
-
- // PIP is unknown
- if (!$row || (strcmp($nodeData['pip'], $row['pluginName']) !== 0)) {
- throw new SystemException("unable to find package installation plugin '".$nodeData['pip']."'");
- }
-
- // valdidate class definition
- $className = $row['className'];
- if (!class_exists($className)) {
- throw new SystemException("unable to find class '".$className."'");
- }
-
- // set default value
- if (empty($nodeData['value'])) {
- $defaultValue = call_user_func([$className, 'getDefaultFilename']);
- if ($defaultValue) {
- $nodeData['value'] = $defaultValue;
- }
- }
-
- $plugin = new $className($this, $nodeData);
-
- if (!($plugin instanceof IPackageInstallationPlugin)) {
- throw new ImplementationException($className, IPackageInstallationPlugin::class);
- }
-
- // execute PIP
- $document = null;
- try {
- $document = $plugin->{$this->action}();
- }
- catch (SplitNodeException $e) {
- $step->setSplitNode($e);
- }
-
- if ($document !== null && ($document instanceof FormDocument)) {
- $step->setDocument($document);
- $step->setSplitNode();
- }
-
- return $step;
- }
-
- /**
- * Displays a list to select optional packages or installs selection.
- *
- * @param string $currentNode
- * @param array $nodeData
- * @return PackageInstallationStep
- */
- protected function selectOptionalPackages($currentNode, array $nodeData) {
- $installationStep = new PackageInstallationStep();
-
- $document = $this->promptOptionalPackages($nodeData);
- if ($document !== null && $document instanceof FormDocument) {
- $installationStep->setDocument($document);
- $installationStep->setSplitNode();
- }
- // insert new nodes for each package
- else if (is_array($document)) {
- // get target child node
- $node = $currentNode;
- $queue = $this->queue;
- $shiftNodes = false;
-
- foreach ($nodeData as $package) {
- if (in_array($package['package'], $document)) {
- // ignore uninstallable packages
- if (!$package['isInstallable']) {
- continue;
- }
-
- if (!$shiftNodes) {
- $this->nodeBuilder->shiftNodes($currentNode, 'tempNode');
- $shiftNodes = true;
- }
-
- $queue = PackageInstallationQueueEditor::create([
- 'parentQueueID' => $queue->queueID,
- 'processNo' => $this->queue->processNo,
- 'userID' => WCF::getUser()->userID,
- 'package' => $package['package'],
- 'packageName' => $package['packageName'],
- 'archive' => $package['archive'],
- 'action' => $queue->action
- ]);
-
- $installation = new PackageInstallationDispatcher($queue);
- $installation->nodeBuilder->setParentNode($node);
- $installation->nodeBuilder->buildNodes();
- $node = $installation->nodeBuilder->getCurrentNode();
- }
- else {
- // remove archive
- @unlink($package['archive']);
- }
- }
-
- // shift nodes
- if ($shiftNodes) {
- $this->nodeBuilder->shiftNodes('tempNode', $node);
- }
- }
-
- return $installationStep;
- }
-
- /**
- * Extracts files from .tar(.gz) archive and installs them
- *
- * @param string $targetDir
- * @param string $sourceArchive
- * @param IFileHandler $fileHandler
- * @return Installer
- */
- public function extractFiles($targetDir, $sourceArchive, $fileHandler = null) {
- return new Installer($targetDir, $sourceArchive, $fileHandler);
- }
-
- /**
- * Returns current package.
- *
- * @return \wcf\data\package\Package
- */
- public function getPackage() {
- if ($this->package === null) {
- $this->package = new Package($this->queue->packageID);
- }
-
- return $this->package;
- }
-
- /**
- * Prompts for a text input for package directory (applies for applications only)
- *
- * @param string $applicationDirectory
- * @return FormDocument
- */
- protected function promptPackageDir($applicationDirectory) {
- // check for pre-defined directories originating from WCFSetup
- $directory = WCF::getSession()->getVar('__wcfSetup_directories');
- $abbreviation = Package::getAbbreviation($this->getPackage()->package);
- if ($directory !== null) {
- $directory = $directory[$abbreviation] ?? null;
- }
- else if (ENABLE_ENTERPRISE_MODE && defined('ENTERPRISE_MODE_APP_DIRECTORIES') && is_array(ENTERPRISE_MODE_APP_DIRECTORIES)) {
- $directory = ENTERPRISE_MODE_APP_DIRECTORIES[$abbreviation] ?? null;
- }
-
- if ($directory === null && !PackageInstallationFormManager::findForm($this->queue, 'packageDir')) {
- $container = new GroupFormElementContainer();
- $packageDir = new TextInputFormElement($container);
- $packageDir->setName('packageDir');
- $packageDir->setLabel(WCF::getLanguage()->get('wcf.acp.package.packageDir.input'));
-
- // check if there are packages installed in a parent
- // directory of WCF, or if packages are below it
- $sql = "SELECT packageDir
- FROM wcf".WCF_N."_package
- WHERE packageDir <> ''";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute();
-
- $isParent = null;
- while ($column = $statement->fetchColumn()) {
- if ($isParent !== null) {
- continue;
- }
-
- if (preg_match('~^\.\./[^\.]~', $column)) {
- $isParent = false;
- }
- else if (mb_strpos($column, '.') !== 0) {
- $isParent = true;
- }
- }
-
- $defaultPath = WCF_DIR;
- if ($isParent === false) {
- $defaultPath = dirname(WCF_DIR);
- }
- if (!$applicationDirectory) {
- $applicationDirectory = Package::getAbbreviation($this->getPackage()->package);
- }
- $defaultPath = FileUtil::addTrailingSlash(FileUtil::unifyDirSeparator($defaultPath)) . $applicationDirectory . '/';
-
- $packageDir->setValue($defaultPath);
- $container->appendChild($packageDir);
-
- $document = new FormDocument('packageDir');
- $document->appendContainer($container);
-
- PackageInstallationFormManager::registerForm($this->queue, $document);
- return $document;
- }
- else {
- if ($directory !== null) {
- $document = null;
- $packageDir = $directory;
- }
- else {
- $document = PackageInstallationFormManager::getForm($this->queue, 'packageDir');
- $document->handleRequest();
- $packageDir = FileUtil::addTrailingSlash(FileUtil::getRealPath(FileUtil::unifyDirSeparator($document->getValue('packageDir'))));
- if ($packageDir === '/') $packageDir = '';
- }
-
- if ($packageDir !== null) {
- // validate package dir
- if ($document !== null && file_exists($packageDir . 'global.php')) {
- $document->setError('packageDir', WCF::getLanguage()->get('wcf.acp.package.packageDir.notAvailable'));
- return $document;
- }
-
- // set package dir
- $packageEditor = new PackageEditor($this->getPackage());
- $packageEditor->update([
- 'packageDir' => FileUtil::getRelativePath(WCF_DIR, $packageDir)
- ]);
-
- // determine domain path, in some environments (e.g. ISPConfig) the $_SERVER paths are
- // faked and differ from the real filesystem path
- if (PACKAGE_ID) {
- $wcfDomainPath = ApplicationHandler::getInstance()->getWCF()->domainPath;
- }
- else {
- $sql = "SELECT domainPath
- FROM wcf".WCF_N."_application
- WHERE packageID = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([1]);
- $row = $statement->fetchArray();
-
- $wcfDomainPath = $row['domainPath'];
- }
-
- $documentRoot = substr(FileUtil::unifyDirSeparator(WCF_DIR), 0, -strlen(FileUtil::unifyDirSeparator($wcfDomainPath)));
- $domainPath = FileUtil::getRelativePath($documentRoot, $packageDir);
- if ($domainPath === './') {
- // `FileUtil::getRelativePath()` returns `./` if both paths lead to the same directory
- $domainPath = '/';
- }
-
- $domainPath = FileUtil::addLeadingSlash($domainPath);
-
- // update application path and untaint application
- $application = new Application($this->getPackage()->packageID);
- $applicationEditor = new ApplicationEditor($application);
- $applicationEditor->update([
- 'domainPath' => $domainPath,
- 'isTainted' => 0,
- ]);
-
- // create directory and set permissions
- @mkdir($packageDir, 0777, true);
- FileUtil::makeWritable($packageDir);
- }
-
- return null;
- }
- }
-
- /**
- * Prompts a selection of optional packages.
- *
- * @param string[][] $packages
- * @return mixed
- */
- protected function promptOptionalPackages(array $packages) {
- if (!PackageInstallationFormManager::findForm($this->queue, 'optionalPackages')) {
- $container = new MultipleSelectionFormElementContainer();
- $container->setName('optionalPackages');
- $container->setLabel(WCF::getLanguage()->get('wcf.acp.package.optionalPackages'));
- $container->setDescription(WCF::getLanguage()->get('wcf.acp.package.optionalPackages.description'));
-
- foreach ($packages as $package) {
- $optionalPackage = new MultipleSelectionFormElement($container);
- $optionalPackage->setName('optionalPackages');
- $optionalPackage->setLabel($package['packageName']);
- $optionalPackage->setValue($package['package']);
- $optionalPackage->setDescription($package['packageDescription']);
- if (!$package['isInstallable']) {
- $optionalPackage->setDisabledMessage(WCF::getLanguage()->get('wcf.acp.package.install.optionalPackage.missingRequirements'));
- }
-
- $container->appendChild($optionalPackage);
- }
-
- $document = new FormDocument('optionalPackages');
- $document->appendContainer($container);
-
- PackageInstallationFormManager::registerForm($this->queue, $document);
- return $document;
- }
- else {
- $document = PackageInstallationFormManager::getForm($this->queue, 'optionalPackages');
- $document->handleRequest();
-
- return $document->getValue('optionalPackages');
- }
- }
-
- /**
- * Returns current package id.
- *
- * @return integer
- */
- public function getPackageID() {
- return $this->queue->packageID;
- }
-
- /**
- * Returns current package name.
- *
- * @return string package name
- * @since 3.0
- */
- public function getPackageName() {
- return $this->queue->packageName;
- }
-
- /**
- * Returns current package installation type.
- *
- * @return string
- */
- public function getAction() {
- return $this->action;
- }
-
- /**
- * Opens the package installation queue and
- * starts the installation, update or uninstallation of the first entry.
- *
- * @param integer $parentQueueID
- * @param integer $processNo
- */
- public static function openQueue($parentQueueID = 0, $processNo = 0) {
- $conditions = new PreparedStatementConditionBuilder();
- $conditions->add("userID = ?", [WCF::getUser()->userID]);
- $conditions->add("parentQueueID = ?", [$parentQueueID]);
- if ($processNo != 0) $conditions->add("processNo = ?", [$processNo]);
- $conditions->add("done = ?", [0]);
-
- $sql = "SELECT *
- FROM wcf".WCF_N."_package_installation_queue
- ".$conditions."
- ORDER BY queueID ASC";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute($conditions->getParameters());
- $packageInstallation = $statement->fetchArray();
-
- if (!isset($packageInstallation['queueID'])) {
- $url = LinkHandler::getInstance()->getLink('PackageList');
- HeaderUtil::redirect($url);
- exit;
- }
- else {
- $url = LinkHandler::getInstance()->getLink('PackageInstallationConfirm', [], 'queueID='.$packageInstallation['queueID']);
- HeaderUtil::redirect($url);
- exit;
- }
- }
-
- /**
- * Checks the package installation queue for outstanding entries.
- *
- * @return integer
- */
- public static function checkPackageInstallationQueue() {
- $sql = "SELECT queueID
- FROM wcf".WCF_N."_package_installation_queue
- WHERE userID = ?
- AND parentQueueID = 0
- AND done = 0
- ORDER BY queueID ASC";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([WCF::getUser()->userID]);
- $row = $statement->fetchArray();
-
- if (!$row) {
- return 0;
- }
-
- return $row['queueID'];
- }
-
- /**
- * Executes post-setup actions.
- */
- public function completeSetup() {
- // remove archives
- $sql = "SELECT archive
- FROM wcf".WCF_N."_package_installation_queue
- WHERE processNo = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([$this->queue->processNo]);
- while ($row = $statement->fetchArray()) {
- @unlink($row['archive']);
- }
-
- // delete queues
- $sql = "DELETE FROM wcf".WCF_N."_package_installation_queue
- WHERE processNo = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([$this->queue->processNo]);
-
- // clear language files once whole installation is completed
- LanguageEditor::deleteLanguageFiles();
-
- // reset all caches
- CacheHandler::getInstance()->flushAll();
- }
-
- /**
- * Updates queue information.
- */
- public function updatePackage() {
- if (empty($this->queue->packageName)) {
- $queueEditor = new PackageInstallationQueueEditor($this->queue);
- $queueEditor->update([
- 'packageName' => $this->getArchive()->getLocalizedPackageInfo('packageName')
- ]);
-
- // reload queue
- $this->queue = new PackageInstallationQueue($this->queue->queueID);
- }
- }
-
- /**
- * Validates specific php requirements.
- *
- * @param array $requirements
- * @return mixed[][]
- */
- public static function validatePHPRequirements(array $requirements) {
- $errors = [];
-
- // validate php version
- if (isset($requirements['version'])) {
- $passed = false;
- if (version_compare(PHP_VERSION, $requirements['version'], '>=')) {
- $passed = true;
- }
-
- if (!$passed) {
- $errors['version'] = [
- 'required' => $requirements['version'],
- 'installed' => PHP_VERSION
- ];
- }
- }
-
- // validate extensions
- if (isset($requirements['extensions'])) {
- foreach ($requirements['extensions'] as $extension) {
- $passed = extension_loaded($extension) ? true : false;
-
- if (!$passed) {
- $errors['extension'][] = [
- 'extension' => $extension
- ];
- }
- }
- }
-
- // validate settings
- if (isset($requirements['settings'])) {
- foreach ($requirements['settings'] as $setting => $value) {
- $iniValue = ini_get($setting);
-
- $passed = self::compareSetting($setting, $value, $iniValue);
- if (!$passed) {
- $errors['setting'][] = [
- 'setting' => $setting,
- 'required' => $value,
- 'installed' => ($iniValue === false) ? '(unknown)' : $iniValue
- ];
- }
- }
- }
-
- // validate functions
- if (isset($requirements['functions'])) {
- foreach ($requirements['functions'] as $function) {
- $function = mb_strtolower($function);
-
- $passed = self::functionExists($function);
- if (!$passed) {
- $errors['function'][] = [
- 'function' => $function
- ];
- }
- }
- }
-
- // validate classes
- if (isset($requirements['classes'])) {
- foreach ($requirements['classes'] as $class) {
- $passed = false;
-
- // see: http://de.php.net/manual/en/language.oop5.basic.php
- if (preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*.~', $class)) {
- $globalClass = '\\'.$class;
-
- if (class_exists($globalClass, false)) {
- $passed = true;
- }
- }
-
- if (!$passed) {
- $errors['class'][] = [
- 'class' => $class
- ];
- }
- }
- }
-
- return $errors;
- }
-
- /**
- * Validates if an function exists and is not blacklisted by suhosin extension.
- *
- * @param string $function
- * @return boolean
- * @see http://de.php.net/manual/en/function.function-exists.php#77980
- */
- protected static function functionExists($function) {
- if (extension_loaded('suhosin')) {
- $blacklist = @ini_get('suhosin.executor.func.blacklist');
- if (!empty($blacklist)) {
- $blacklist = explode(',', $blacklist);
- foreach ($blacklist as $disabledFunction) {
- $disabledFunction = mb_strtolower(StringUtil::trim($disabledFunction));
-
- if ($function == $disabledFunction) {
- return false;
- }
- }
- }
- }
-
- return function_exists($function);
- }
-
- /**
- * Compares settings, converting values into compareable ones.
- *
- * @param string $setting
- * @param string $value
- * @param mixed $compareValue
- * @return boolean
- */
- protected static function compareSetting($setting, $value, $compareValue) {
- if ($compareValue === false) return false;
-
- $value = mb_strtolower($value);
- $trueValues = ['1', 'on', 'true'];
- $falseValues = ['0', 'off', 'false'];
-
- // handle values considered as 'true'
- if (in_array($value, $trueValues)) {
- return $compareValue ? true : false;
- }
- // handle values considered as 'false'
- else if (in_array($value, $falseValues)) {
- return (!$compareValue) ? true : false;
- }
- else if (!is_numeric($value)) {
- $compareValue = self::convertShorthandByteValue($compareValue);
- $value = self::convertShorthandByteValue($value);
- }
-
- return ($compareValue >= $value) ? true : false;
- }
-
- /**
- * Converts shorthand byte values into an integer representing bytes.
- *
- * @param string $value
- * @return integer
- * @see http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
- */
- protected static function convertShorthandByteValue($value) {
- // convert into bytes
- $lastCharacter = mb_substr($value, -1);
- switch ($lastCharacter) {
- // gigabytes
- case 'g':
- return (int)$value * 1073741824;
- break;
-
- // megabytes
- case 'm':
- return (int)$value * 1048576;
- break;
-
- // kilobytes
- case 'k':
- return (int)$value * 1024;
- break;
-
- default:
- return $value;
- break;
- }
- }
+class PackageInstallationDispatcher
+{
+ /**
+ * current installation type
+ * @var string
+ */
+ protected $action = '';
+
+ /**
+ * instance of PackageArchive
+ * @var PackageArchive
+ */
+ public $archive;
+
+ /**
+ * instance of PackageInstallationNodeBuilder
+ * @var PackageInstallationNodeBuilder
+ */
+ public $nodeBuilder;
+
+ /**
+ * instance of Package
+ * @var Package
+ */
+ public $package;
+
+ /**
+ * instance of PackageInstallationQueue
+ * @var PackageInstallationQueue
+ */
+ public $queue;
+
+ /**
+ * default name of the config file
+ * @var string
+ */
+ const CONFIG_FILE = 'app.config.inc.php';
+
+ /**
+ * data of previous package in queue
+ * @var string[]
+ */
+ protected $previousPackageData;
+
+ /**
+ * Creates a new instance of PackageInstallationDispatcher.
+ *
+ * @param PackageInstallationQueue $queue
+ */
+ public function __construct(PackageInstallationQueue $queue)
+ {
+ $this->queue = $queue;
+ $this->nodeBuilder = new PackageInstallationNodeBuilder($this);
+
+ $this->action = $this->queue->action;
+ }
+
+ /**
+ * Sets data of previous package in queue.
+ *
+ * @param string[] $packageData
+ */
+ public function setPreviousPackage(array $packageData)
+ {
+ $this->previousPackageData = $packageData;
+ }
+
+ /**
+ * Installs node components and returns next node.
+ *
+ * @param string $node
+ * @return PackageInstallationStep
+ * @throws SystemException
+ */
+ public function install($node)
+ {
+ $nodes = $this->nodeBuilder->getNodeData($node);
+ if (empty($nodes)) {
+ // guard against possible issues with empty instruction blocks, including
+ // these blocks that contain no valid instructions at all (e.g. typo from
+ // copy & paste)
+ throw new SystemException(
+ "Failed to retrieve nodes for identifier '{$node}', the query returned no results."
+ );
+ }
+
+ // invoke node-specific actions
+ $step = null;
+ foreach ($nodes as $data) {
+ $nodeData = \unserialize($data['nodeData']);
+ $this->logInstallationStep($data);
+
+ switch ($data['nodeType']) {
+ case 'package':
+ $step = $this->installPackage($nodeData);
+ break;
+
+ case 'pip':
+ $step = $this->executePIP($nodeData);
+ break;
+
+ case 'optionalPackages':
+ $step = $this->selectOptionalPackages($node, $nodeData);
+ break;
+
+ default:
+ exit("Unknown node type: '" . $data['nodeType'] . "'");
+ break;
+ }
+
+ if ($step->splitNode()) {
+ $log = 'split node';
+ if ($step->getException() !== null && $step->getException()->getMessage()) {
+ $log .= ': ' . $step->getException()->getMessage();
+ }
+
+ $this->logInstallationStep($data, $log);
+ $this->nodeBuilder->cloneNode($node, $data['sequenceNo']);
+ break;
+ }
+ }
+
+ // mark node as completed
+ $this->nodeBuilder->completeNode($node);
+
+ // assign next node
+ $node = $this->nodeBuilder->getNextNode($node);
+ $step->setNode($node);
+
+ // perform post-install/update actions
+ if ($node == '') {
+ $this->logInstallationStep([], 'start cleanup');
+
+ // update "last update time" option
+ $sql = "UPDATE wcf" . WCF_N . "_option
+ SET optionValue = ?
+ WHERE optionName = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([
+ TIME_NOW,
+ 'last_update_time',
+ ]);
+
+ // update options.inc.php
+ OptionEditor::resetCache();
+
+ if ($this->action == 'install') {
+ // save localized package infos
+ $this->saveLocalizedPackageInfos();
+
+ // remove all cache files after WCFSetup
+ if (!PACKAGE_ID) {
+ CacheHandler::getInstance()->flushAll();
+
+ $sql = "UPDATE wcf" . WCF_N . "_option
+ SET optionValue = ?
+ WHERE optionName = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+
+ $statement->execute([
+ StringUtil::getUUID(),
+ 'wcf_uuid',
+ ]);
+
+ if (\file_exists(WCF_DIR . 'cookiePrefix.txt')) {
+ $statement->execute([
+ COOKIE_PREFIX,
+ 'cookie_prefix',
+ ]);
+
+ @\unlink(WCF_DIR . 'cookiePrefix.txt');
+ }
+
+ $user = new User(1);
+ $statement->execute([
+ $user->username,
+ 'mail_from_name',
+ ]);
+ $statement->execute([
+ $user->email,
+ 'mail_from_address',
+ ]);
+ $statement->execute([
+ $user->email,
+ 'mail_admin_address',
+ ]);
+
+ $statement->execute([
+ // We do not use the cache-timing safe class Hex, because we run the
+ // function during the setup.
+ $signatureSecret = \bin2hex(\random_bytes(20)),
+ 'signature_secret',
+ ]);
+ \define('SIGNATURE_SECRET', $signatureSecret);
+ HeaderUtil::setCookie(
+ 'user_session',
+ // We do not use the cache-timing safe class Hex, because we run the
+ // function during the setup.
+ CryptoUtil::createSignedString(
+ \pack(
+ 'CA20C',
+ 1,
+ \hex2bin(WCF::getSession()->sessionID),
+ 0
+ )
+ )
+ );
+
+ if (WCF::getSession()->getVar('__wcfSetup_developerMode')) {
+ $statement->execute([
+ 1,
+ 'enable_debug_mode',
+ ]);
+ $statement->execute([
+ 'public',
+ 'exception_privacy',
+ ]);
+ $statement->execute([
+ 'debugFolder',
+ 'mail_send_method',
+ ]);
+ $statement->execute([
+ 1,
+ 'enable_developer_tools',
+ ]);
+ $statement->execute([
+ 1,
+ 'log_missing_language_items',
+ ]);
+
+ foreach (DevtoolsSetup::getInstance()->getOptionOverrides() as $optionName => $optionValue) {
+ $statement->execute([
+ $optionValue,
+ $optionName,
+ ]);
+ }
+
+ foreach (DevtoolsSetup::getInstance()->getUsers() as $newUser) {
+ try {
+ (new UserAction([], 'create', [
+ 'data' => [
+ 'email' => $newUser['email'],
+ 'password' => $newUser['password'],
+ 'username' => $newUser['username'],
+ ],
+ 'groups' => [
+ 1,
+ 3,
+ ],
+ ]))->executeAction();
+ } catch (SystemException $e) {
+ // ignore errors due to event listeners missing at this
+ // point during installation
+ }
+ }
+
+ if (($importPath = DevtoolsSetup::getInstance()->getDevtoolsImportPath()) !== '') {
+ (new DevtoolsProjectAction([], 'quickSetup', [
+ 'path' => $importPath,
+ ]))->executeAction();
+ }
+ }
+
+ if (WCF::getSession()->getVar('__wcfSetup_imagick')) {
+ $statement->execute([
+ 'imagick',
+ 'image_adapter_type',
+ ]);
+ }
+
+ // update options.inc.php
+ OptionEditor::resetCache();
+
+ WCF::getSession()->register('__wcfSetup_completed', true);
+ }
+
+ // rebuild application paths
+ ApplicationHandler::rebuild();
+ }
+
+ // remove template listener cache
+ TemplateListenerCodeCacheBuilder::getInstance()->reset();
+
+ // reset language cache
+ LanguageFactory::getInstance()->clearCache();
+ LanguageFactory::getInstance()->deleteLanguageCache();
+
+ // reset stylesheets
+ StyleHandler::resetStylesheets();
+
+ // clear user storage
+ UserStorageHandler::getInstance()->clear();
+
+ // rebuild config files for affected applications
+ $sql = "SELECT package.packageID
+ FROM wcf" . WCF_N . "_package_installation_queue queue,
+ wcf" . WCF_N . "_package package
+ WHERE queue.processNo = ?
+ AND package.packageID = queue.packageID
+ AND package.isApplication = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([
+ $this->queue->processNo,
+ 1,
+ ]);
+ while ($row = $statement->fetchArray()) {
+ Package::writeConfigFile($row['packageID']);
+ }
+
+ EventHandler::getInstance()->fireAction($this, 'postInstall');
+
+ // remove archives
+ $sql = "SELECT archive
+ FROM wcf" . WCF_N . "_package_installation_queue
+ WHERE processNo = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$this->queue->processNo]);
+ while ($row = $statement->fetchArray()) {
+ @\unlink($row['archive']);
+ }
+
+ // delete queues
+ $sql = "DELETE FROM wcf" . WCF_N . "_package_installation_queue
+ WHERE processNo = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$this->queue->processNo]);
+
+ $this->logInstallationStep([], 'finished cleanup');
+ }
+
+ return $step;
+ }
+
+ /**
+ * Logs an installation step.
+ *
+ * @param array $node data of the executed node
+ * @param string $log optional additional log text
+ */
+ protected function logInstallationStep(array $node = [], $log = '')
+ {
+ $logEntry = "[" . TIME_NOW . "]\n";
+ if (!empty($node)) {
+ $logEntry .= 'sequenceNo: ' . $node['sequenceNo'] . "\n";
+ $logEntry .= 'nodeType: ' . $node['nodeType'] . "\n";
+ $logEntry .= "nodeData:\n";
+
+ $nodeData = \unserialize($node['nodeData']);
+ foreach ($nodeData as $index => $value) {
+ $logEntry .= "\t" . $index . ': ' . (!\is_object($value) && !\is_array($value) ? $value : JSON::encode($value)) . "\n";
+ }
+ }
+
+ if ($log) {
+ $logEntry .= 'additional information: ' . $log . "\n";
+ }
+
+ $logEntry .= \str_repeat('-', 30) . "\n\n";
+
+ \file_put_contents(
+ WCF_DIR . 'log/' . \date('Y-m-d', TIME_NOW) . '-update-' . $this->queue->queueID . '.txt',
+ $logEntry,
+ \FILE_APPEND
+ );
+ }
+
+ /**
+ * Returns current package archive.
+ *
+ * @return PackageArchive
+ */
+ public function getArchive()
+ {
+ if ($this->archive === null) {
+ // check if we're doing an iterative update of the same package
+ if (
+ $this->previousPackageData !== null
+ && $this->getPackage()->package == $this->previousPackageData['package']
+ ) {
+ if (
+ Package::compareVersion(
+ $this->getPackage()->packageVersion,
+ $this->previousPackageData['packageVersion'],
+ '<'
+ )
+ ) {
+ // fake package to simulate the package version required by current archive
+ $this->getPackage()->setPackageVersion($this->previousPackageData['packageVersion']);
+ }
+ }
+
+ $this->archive = new PackageArchive($this->queue->archive, $this->getPackage());
+
+ if (FileUtil::isURL($this->archive->getArchive())) {
+ // get return value and update entry in
+ // package_installation_queue with this value
+ $archive = $this->archive->downloadArchive();
+ $queueEditor = new PackageInstallationQueueEditor($this->queue);
+ $queueEditor->update(['archive' => $archive]);
+ }
+
+ $this->archive->openArchive();
+ }
+
+ return $this->archive;
+ }
+
+ /**
+ * Installs current package.
+ *
+ * @param mixed[] $nodeData
+ * @return PackageInstallationStep
+ * @throws SystemException
+ */
+ protected function installPackage(array $nodeData)
+ {
+ $installationStep = new PackageInstallationStep();
+
+ // check requirements
+ if (!empty($nodeData['requirements'])) {
+ foreach ($nodeData['requirements'] as $package => $requirementData) {
+ // get existing package
+ if ($requirementData['packageID']) {
+ $sql = "SELECT packageName, packageVersion
+ FROM wcf" . WCF_N . "_package
+ WHERE packageID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$requirementData['packageID']]);
+ } else {
+ // try to find matching package
+ $sql = "SELECT packageName, packageVersion
+ FROM wcf" . WCF_N . "_package
+ WHERE package = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$package]);
+ }
+ $row = $statement->fetchArray();
+
+ // package is required but not available
+ if ($row === false) {
+ throw new SystemException("Package '" . $package . "' is required by '" . $nodeData['packageName'] . "', but is neither installed nor shipped.");
+ }
+
+ // check version requirements
+ if ($requirementData['minVersion']) {
+ if (Package::compareVersion($row['packageVersion'], $requirementData['minVersion']) < 0) {
+ throw new SystemException("Package '" . $nodeData['packageName'] . "' requires package '" . $row['packageName'] . "' in version '" . $requirementData['minVersion'] . "', but only version '" . $row['packageVersion'] . "' is installed");
+ }
+ }
+ }
+ }
+ unset($nodeData['requirements']);
+
+ $applicationDirectory = '';
+ if (isset($nodeData['applicationDirectory'])) {
+ $applicationDirectory = $nodeData['applicationDirectory'];
+ unset($nodeData['applicationDirectory']);
+ }
+
+ // update package
+ if ($this->queue->packageID) {
+ $packageEditor = new PackageEditor(new Package($this->queue->packageID));
+ unset($nodeData['installDate']);
+ $packageEditor->update($nodeData);
+
+ // delete old excluded packages
+ $sql = "DELETE FROM wcf" . WCF_N . "_package_exclusion
+ WHERE packageID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$this->queue->packageID]);
+
+ // delete old compatibility versions
+ $sql = "DELETE FROM wcf" . WCF_N . "_package_compatibility
+ WHERE packageID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$this->queue->packageID]);
+
+ // delete old requirements and dependencies
+ $sql = "DELETE FROM wcf" . WCF_N . "_package_requirement
+ WHERE packageID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$this->queue->packageID]);
+ } else {
+ // create package entry
+ $package = $this->createPackage($nodeData);
+
+ // update package id for current queue
+ $queueEditor = new PackageInstallationQueueEditor($this->queue);
+ $queueEditor->update(['packageID' => $package->packageID]);
+
+ // reload queue
+ $this->queue = new PackageInstallationQueue($this->queue->queueID);
+ $this->package = null;
+
+ if ($package->isApplication) {
+ $host = \str_replace(RouteHandler::getProtocol(), '', RouteHandler::getHost());
+ $path = RouteHandler::getPath(['acp']);
+
+ // insert as application
+ ApplicationEditor::create([
+ 'domainName' => $host,
+ 'domainPath' => $path,
+ 'cookieDomain' => $host,
+ 'packageID' => $package->packageID,
++ 'isTainted' => 1,
+ ]);
+ }
+ }
+
+ // save excluded packages
+ if (\count($this->getArchive()->getExcludedPackages())) {
+ $sql = "INSERT INTO wcf" . WCF_N . "_package_exclusion
+ (packageID, excludedPackage, excludedPackageVersion)
+ VALUES (?, ?, ?)";
+ $statement = WCF::getDB()->prepareStatement($sql);
+
+ foreach ($this->getArchive()->getExcludedPackages() as $excludedPackage) {
+ $statement->execute([
+ $this->queue->packageID,
+ $excludedPackage['name'],
+ !empty($excludedPackage['version']) ? $excludedPackage['version'] : '',
+ ]);
+ }
+ }
+
+ // save compatible versions
+ if (!empty($this->getArchive()->getCompatibleVersions())) {
+ $sql = "INSERT INTO wcf" . WCF_N . "_package_compatibility
+ (packageID, version)
+ VALUES (?, ?)";
+ $statement = WCF::getDB()->prepareStatement($sql);
+
+ foreach ($this->getArchive()->getCompatibleVersions() as $version) {
+ $statement->execute([
+ $this->queue->packageID,
+ $version,
+ ]);
+ }
+ }
+
+ // insert requirements and dependencies
+ $requirements = $this->getArchive()->getAllExistingRequirements();
+ if (!empty($requirements)) {
+ $sql = "INSERT INTO wcf" . WCF_N . "_package_requirement
+ (packageID, requirement)
+ VALUES (?, ?)";
+ $statement = WCF::getDB()->prepareStatement($sql);
+
+ foreach ($requirements as $identifier => $possibleRequirements) {
+ $requirement = \array_shift($possibleRequirements);
+
+ $statement->execute([
+ $this->queue->packageID,
+ $requirement['packageID'],
+ ]);
+ }
+ }
+
+ if (
+ $this->getPackage()->isApplication
+ && $this->getPackage()->package != 'com.woltlab.wcf'
+ && $this->getAction() == 'install'
+ && empty($this->getPackage()->packageDir)
+ ) {
+ $document = $this->promptPackageDir($applicationDirectory);
+ if ($document !== null && $document instanceof FormDocument) {
+ $installationStep->setDocument($document);
+ }
+
+ $installationStep->setSplitNode();
+ }
+
+ return $installationStep;
+ }
+
+ /**
+ * Creates a new package based on the given data and returns it.
+ *
+ * @param array $packageData
+ * @return Package
+ * @since 5.2
+ */
+ protected function createPackage(array $packageData)
+ {
+ return PackageEditor::create($packageData);
+ }
+
+ /**
+ * Saves the localized package info.
+ */
+ protected function saveLocalizedPackageInfos()
+ {
+ $package = new Package($this->queue->packageID);
+
+ // localize package information
+ $sql = "INSERT INTO wcf" . WCF_N . "_language_item
+ (languageID, languageItem, languageItemValue, languageCategoryID, packageID)
+ VALUES (?, ?, ?, ?, ?)";
+ $statement = WCF::getDB()->prepareStatement($sql);
+
+ // get language list
+ $languageList = new LanguageList();
+ $languageList->readObjects();
+
+ // workaround for WCFSetup
+ if (!PACKAGE_ID) {
+ $sql = "SELECT *
+ FROM wcf" . WCF_N . "_language_category
+ WHERE languageCategory = ?";
+ $statement2 = WCF::getDB()->prepareStatement($sql);
+ $statement2->execute(['wcf.acp.package']);
+ $languageCategory = $statement2->fetchObject(LanguageCategory::class);
+ } else {
+ $languageCategory = LanguageFactory::getInstance()->getCategory('wcf.acp.package');
+ }
+
+ // save package name
+ $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageName');
+
+ // save package description
+ $this->saveLocalizedPackageInfo($statement, $languageList, $languageCategory, $package, 'packageDescription');
+
+ // update description and name
+ $packageEditor = new PackageEditor($package);
+ $packageEditor->update([
+ 'packageDescription' => 'wcf.acp.package.packageDescription.package' . $this->queue->packageID,
+ 'packageName' => 'wcf.acp.package.packageName.package' . $this->queue->packageID,
+ ]);
+ }
+
+ /**
+ * Saves a localized package info.
+ *
+ * @param PreparedStatement $statement
+ * @param LanguageList $languageList
+ * @param LanguageCategory $languageCategory
+ * @param Package $package
+ * @param string $infoName
+ */
+ protected function saveLocalizedPackageInfo(
+ PreparedStatement $statement,
+ $languageList,
+ LanguageCategory $languageCategory,
+ Package $package,
+ $infoName
+ ) {
+ $infoValues = $this->getArchive()->getPackageInfo($infoName);
+
+ // get default value for languages without specified information
+ $defaultValue = '';
+ if (isset($infoValues['default'])) {
+ $defaultValue = $infoValues['default'];
+ } elseif (isset($infoValues['en'])) {
+ // fallback to English
+ $defaultValue = $infoValues['en'];
+ } elseif (isset($infoValues[WCF::getLanguage()->getFixedLanguageCode()])) {
+ // fallback to the language of the current user
+ $defaultValue = $infoValues[WCF::getLanguage()->getFixedLanguageCode()];
+ } elseif ($infoName == 'packageName') {
+ // fallback to the package identifier for the package name
+ $defaultValue = $this->getArchive()->getPackageInfo('name');
+ }
+
+ foreach ($languageList as $language) {
+ $value = $defaultValue;
+ if (isset($infoValues[$language->languageCode])) {
+ $value = $infoValues[$language->languageCode];
+ }
+
+ $statement->execute([
+ $language->languageID,
+ 'wcf.acp.package.' . $infoName . '.package' . $package->packageID,
+ $value,
+ $languageCategory->languageCategoryID,
+ 1,
+ ]);
+ }
+ }
+
+ /**
+ * Executes a package installation plugin.
+ *
+ * @param mixed[] $nodeData
+ * @return PackageInstallationStep
+ * @throws SystemException
+ */
+ protected function executePIP(array $nodeData)
+ {
+ $step = new PackageInstallationStep();
+
+ if ($nodeData['pip'] == PackageArchive::VOID_MARKER) {
+ return $step;
+ }
+
+ // fetch all pips associated with current PACKAGE_ID and include pips
+ // previously installed by current installation queue
+ $sql = "SELECT pluginName, className
+ FROM wcf" . WCF_N . "_package_installation_plugin
+ WHERE pluginName = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$nodeData['pip']]);
+ $row = $statement->fetchArray();
+
+ // PIP is unknown
+ if (!$row || (\strcmp($nodeData['pip'], $row['pluginName']) !== 0)) {
+ throw new SystemException("unable to find package installation plugin '" . $nodeData['pip'] . "'");
+ }
+
+ // valdidate class definition
+ $className = $row['className'];
+ if (!\class_exists($className)) {
+ throw new SystemException("unable to find class '" . $className . "'");
+ }
+
+ // set default value
+ if (empty($nodeData['value'])) {
+ $defaultValue = \call_user_func([$className, 'getDefaultFilename']);
+ if ($defaultValue) {
+ $nodeData['value'] = $defaultValue;
+ }
+ }
+
+ $plugin = new $className($this, $nodeData);
+
+ if (!($plugin instanceof IPackageInstallationPlugin)) {
+ throw new ImplementationException($className, IPackageInstallationPlugin::class);
+ }
+
+ // execute PIP
+ $document = null;
+ try {
+ $document = $plugin->{$this->action}();
+ } catch (SplitNodeException $e) {
+ $step->setSplitNode($e);
+ }
+
+ if ($document !== null && ($document instanceof FormDocument)) {
+ $step->setDocument($document);
+ $step->setSplitNode();
+ }
+
+ return $step;
+ }
+
+ /**
+ * Displays a list to select optional packages or installs selection.
+ *
+ * @param string $currentNode
+ * @param array $nodeData
+ * @return PackageInstallationStep
+ */
+ protected function selectOptionalPackages($currentNode, array $nodeData)
+ {
+ $installationStep = new PackageInstallationStep();
+
+ $document = $this->promptOptionalPackages($nodeData);
+ if ($document !== null && $document instanceof FormDocument) {
+ $installationStep->setDocument($document);
+ $installationStep->setSplitNode();
+ } // insert new nodes for each package
+ elseif (\is_array($document)) {
+ // get target child node
+ $node = $currentNode;
+ $queue = $this->queue;
+ $shiftNodes = false;
+
+ foreach ($nodeData as $package) {
+ if (\in_array($package['package'], $document)) {
+ // ignore uninstallable packages
+ if (!$package['isInstallable']) {
+ continue;
+ }
+
+ if (!$shiftNodes) {
+ $this->nodeBuilder->shiftNodes($currentNode, 'tempNode');
+ $shiftNodes = true;
+ }
+
+ $queue = PackageInstallationQueueEditor::create([
+ 'parentQueueID' => $queue->queueID,
+ 'processNo' => $this->queue->processNo,
+ 'userID' => WCF::getUser()->userID,
+ 'package' => $package['package'],
+ 'packageName' => $package['packageName'],
+ 'archive' => $package['archive'],
+ 'action' => $queue->action,
+ ]);
+
+ $installation = new self($queue);
+ $installation->nodeBuilder->setParentNode($node);
+ $installation->nodeBuilder->buildNodes();
+ $node = $installation->nodeBuilder->getCurrentNode();
+ } else {
+ // remove archive
+ @\unlink($package['archive']);
+ }
+ }
+
+ // shift nodes
+ if ($shiftNodes) {
+ $this->nodeBuilder->shiftNodes('tempNode', $node);
+ }
+ }
+
+ return $installationStep;
+ }
+
+ /**
+ * Extracts files from .tar(.gz) archive and installs them
+ *
+ * @param string $targetDir
+ * @param string $sourceArchive
+ * @param IFileHandler $fileHandler
+ * @return Installer
+ */
+ public function extractFiles($targetDir, $sourceArchive, $fileHandler = null)
+ {
+ return new Installer($targetDir, $sourceArchive, $fileHandler);
+ }
+
+ /**
+ * Returns current package.
+ *
+ * @return \wcf\data\package\Package
+ */
+ public function getPackage()
+ {
+ if ($this->package === null) {
+ $this->package = new Package($this->queue->packageID);
+ }
+
+ return $this->package;
+ }
+
+ /**
+ * Prompts for a text input for package directory (applies for applications only)
+ *
+ * @param string $applicationDirectory
+ * @return FormDocument
+ */
+ protected function promptPackageDir($applicationDirectory)
+ {
+ // check for pre-defined directories originating from WCFSetup
+ $directory = WCF::getSession()->getVar('__wcfSetup_directories');
+ $abbreviation = Package::getAbbreviation($this->getPackage()->package);
+ if ($directory !== null) {
+ $directory = $directory[$abbreviation] ?? null;
+ } elseif (
+ ENABLE_ENTERPRISE_MODE
+ && \defined('ENTERPRISE_MODE_APP_DIRECTORIES')
+ && \is_array(ENTERPRISE_MODE_APP_DIRECTORIES)
+ ) {
+ $directory = ENTERPRISE_MODE_APP_DIRECTORIES[$abbreviation] ?? null;
+ }
+
+ if ($directory === null && !PackageInstallationFormManager::findForm($this->queue, 'packageDir')) {
+ $container = new GroupFormElementContainer();
+ $packageDir = new TextInputFormElement($container);
+ $packageDir->setName('packageDir');
+ $packageDir->setLabel(WCF::getLanguage()->get('wcf.acp.package.packageDir.input'));
+
+ // check if there are packages installed in a parent
+ // directory of WCF, or if packages are below it
+ $sql = "SELECT packageDir
+ FROM wcf" . WCF_N . "_package
+ WHERE packageDir <> ''";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute();
+
+ $isParent = null;
+ while ($column = $statement->fetchColumn()) {
+ if ($isParent !== null) {
+ continue;
+ }
+
+ if (\preg_match('~^\.\./[^\.]~', $column)) {
+ $isParent = false;
+ } elseif (\mb_strpos($column, '.') !== 0) {
+ $isParent = true;
+ }
+ }
+
+ $defaultPath = WCF_DIR;
+ if ($isParent === false) {
+ $defaultPath = \dirname(WCF_DIR);
+ }
+ if (!$applicationDirectory) {
+ $applicationDirectory = Package::getAbbreviation($this->getPackage()->package);
+ }
+ $defaultPath = FileUtil::addTrailingSlash(FileUtil::unifyDirSeparator($defaultPath)) . $applicationDirectory . '/';
+
+ $packageDir->setValue($defaultPath);
+ $container->appendChild($packageDir);
+
+ $document = new FormDocument('packageDir');
+ $document->appendContainer($container);
+
+ PackageInstallationFormManager::registerForm($this->queue, $document);
+
+ return $document;
+ } else {
+ if ($directory !== null) {
+ $document = null;
+ $packageDir = $directory;
+ } else {
+ $document = PackageInstallationFormManager::getForm($this->queue, 'packageDir');
+ $document->handleRequest();
+ $packageDir = FileUtil::addTrailingSlash(FileUtil::getRealPath(FileUtil::unifyDirSeparator(
+ $document->getValue('packageDir')
+ )));
+ if ($packageDir === '/') {
+ $packageDir = '';
+ }
+ }
+
+ if ($packageDir !== null) {
+ // validate package dir
+ if ($document !== null && \file_exists($packageDir . 'global.php')) {
+ $document->setError(
+ 'packageDir',
+ WCF::getLanguage()->get('wcf.acp.package.packageDir.notAvailable')
+ );
+
+ return $document;
+ }
+
+ // set package dir
+ $packageEditor = new PackageEditor($this->getPackage());
+ $packageEditor->update([
+ 'packageDir' => FileUtil::getRelativePath(WCF_DIR, $packageDir),
+ ]);
+
+ // determine domain path, in some environments (e.g. ISPConfig) the $_SERVER paths are
+ // faked and differ from the real filesystem path
+ if (PACKAGE_ID) {
+ $wcfDomainPath = ApplicationHandler::getInstance()->getWCF()->domainPath;
+ } else {
+ $sql = "SELECT domainPath
+ FROM wcf" . WCF_N . "_application
+ WHERE packageID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([1]);
+ $row = $statement->fetchArray();
+
+ $wcfDomainPath = $row['domainPath'];
+ }
+
+ $documentRoot = \substr(
+ FileUtil::unifyDirSeparator(WCF_DIR),
+ 0,
+ -\strlen(FileUtil::unifyDirSeparator($wcfDomainPath))
+ );
+ $domainPath = FileUtil::getRelativePath($documentRoot, $packageDir);
+ if ($domainPath === './') {
+ // `FileUtil::getRelativePath()` returns `./` if both paths lead to the same directory
+ $domainPath = '/';
+ }
+
+ $domainPath = FileUtil::addLeadingSlash($domainPath);
+
- // update application path
++ // update application path and untaint application
+ $application = new Application($this->getPackage()->packageID);
+ $applicationEditor = new ApplicationEditor($application);
- $applicationEditor->update(['domainPath' => $domainPath]);
++ $applicationEditor->update([
++ 'domainPath' => $domainPath,
++ 'isTainted' => 0,
++ ]);
+
+ // create directory and set permissions
+ @\mkdir($packageDir, 0777, true);
+ FileUtil::makeWritable($packageDir);
+ }
+
+ return;
+ }
+ }
+
+ /**
+ * Prompts a selection of optional packages.
+ *
+ * @param string[][] $packages
+ * @return mixed
+ */
+ protected function promptOptionalPackages(array $packages)
+ {
+ if (!PackageInstallationFormManager::findForm($this->queue, 'optionalPackages')) {
+ $container = new MultipleSelectionFormElementContainer();
+ $container->setName('optionalPackages');
+ $container->setLabel(WCF::getLanguage()->get('wcf.acp.package.optionalPackages'));
+ $container->setDescription(WCF::getLanguage()->get('wcf.acp.package.optionalPackages.description'));
+
+ foreach ($packages as $package) {
+ $optionalPackage = new MultipleSelectionFormElement($container);
+ $optionalPackage->setName('optionalPackages');
+ $optionalPackage->setLabel($package['packageName']);
+ $optionalPackage->setValue($package['package']);
+ $optionalPackage->setDescription($package['packageDescription']);
+ if (!$package['isInstallable']) {
+ $optionalPackage->setDisabledMessage(
+ WCF::getLanguage()->get('wcf.acp.package.install.optionalPackage.missingRequirements')
+ );
+ }
+
+ $container->appendChild($optionalPackage);
+ }
+
+ $document = new FormDocument('optionalPackages');
+ $document->appendContainer($container);
+
+ PackageInstallationFormManager::registerForm($this->queue, $document);
+
+ return $document;
+ } else {
+ $document = PackageInstallationFormManager::getForm($this->queue, 'optionalPackages');
+ $document->handleRequest();
+
+ return $document->getValue('optionalPackages');
+ }
+ }
+
+ /**
+ * Returns current package id.
+ *
+ * @return int
+ */
+ public function getPackageID()
+ {
+ return $this->queue->packageID;
+ }
+
+ /**
+ * Returns current package name.
+ *
+ * @return string package name
+ * @since 3.0
+ */
+ public function getPackageName()
+ {
+ return $this->queue->packageName;
+ }
+
+ /**
+ * Returns current package installation type.
+ *
+ * @return string
+ */
+ public function getAction()
+ {
+ return $this->action;
+ }
+
+ /**
+ * Opens the package installation queue and
+ * starts the installation, update or uninstallation of the first entry.
+ *
+ * @param int $parentQueueID
+ * @param int $processNo
+ */
+ public static function openQueue($parentQueueID = 0, $processNo = 0)
+ {
+ $conditions = new PreparedStatementConditionBuilder();
+ $conditions->add("userID = ?", [WCF::getUser()->userID]);
+ $conditions->add("parentQueueID = ?", [$parentQueueID]);
+ if ($processNo != 0) {
+ $conditions->add("processNo = ?", [$processNo]);
+ }
+ $conditions->add("done = ?", [0]);
+
+ $sql = "SELECT *
+ FROM wcf" . WCF_N . "_package_installation_queue
+ " . $conditions . "
+ ORDER BY queueID ASC";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute($conditions->getParameters());
+ $packageInstallation = $statement->fetchArray();
+
+ if (!isset($packageInstallation['queueID'])) {
+ $url = LinkHandler::getInstance()->getLink('PackageList');
+ HeaderUtil::redirect($url);
+
+ exit;
+ } else {
+ $url = LinkHandler::getInstance()->getLink(
+ 'PackageInstallationConfirm',
+ [],
+ 'queueID=' . $packageInstallation['queueID']
+ );
+ HeaderUtil::redirect($url);
+
+ exit;
+ }
+ }
+
+ /**
+ * Checks the package installation queue for outstanding entries.
+ *
+ * @return int
+ */
+ public static function checkPackageInstallationQueue()
+ {
+ $sql = "SELECT queueID
+ FROM wcf" . WCF_N . "_package_installation_queue
+ WHERE userID = ?
+ AND parentQueueID = 0
+ AND done = 0
+ ORDER BY queueID ASC";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([WCF::getUser()->userID]);
+ $row = $statement->fetchArray();
+
+ if (!$row) {
+ return 0;
+ }
+
+ return $row['queueID'];
+ }
+
+ /**
+ * Executes post-setup actions.
+ */
+ public function completeSetup()
+ {
+ // remove archives
+ $sql = "SELECT archive
+ FROM wcf" . WCF_N . "_package_installation_queue
+ WHERE processNo = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$this->queue->processNo]);
+ while ($row = $statement->fetchArray()) {
+ @\unlink($row['archive']);
+ }
+
+ // delete queues
+ $sql = "DELETE FROM wcf" . WCF_N . "_package_installation_queue
+ WHERE processNo = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$this->queue->processNo]);
+
+ // clear language files once whole installation is completed
+ LanguageEditor::deleteLanguageFiles();
+
+ // reset all caches
+ CacheHandler::getInstance()->flushAll();
+ }
+
+ /**
+ * Updates queue information.
+ */
+ public function updatePackage()
+ {
+ if (empty($this->queue->packageName)) {
+ $queueEditor = new PackageInstallationQueueEditor($this->queue);
+ $queueEditor->update([
+ 'packageName' => $this->getArchive()->getLocalizedPackageInfo('packageName'),
+ ]);
+
+ // reload queue
+ $this->queue = new PackageInstallationQueue($this->queue->queueID);
+ }
+ }
+
+ /**
+ * Validates specific php requirements.
+ *
+ * @param array $requirements
+ * @return mixed[][]
+ */
+ public static function validatePHPRequirements(array $requirements)
+ {
+ $errors = [];
+
+ // validate php version
+ if (isset($requirements['version'])) {
+ $passed = false;
+ if (\version_compare(\PHP_VERSION, $requirements['version'], '>=')) {
+ $passed = true;
+ }
+
+ if (!$passed) {
+ $errors['version'] = [
+ 'required' => $requirements['version'],
+ 'installed' => \PHP_VERSION,
+ ];
+ }
+ }
+
+ // validate extensions
+ if (isset($requirements['extensions'])) {
+ foreach ($requirements['extensions'] as $extension) {
+ $passed = \extension_loaded($extension) ? true : false;
+
+ if (!$passed) {
+ $errors['extension'][] = [
+ 'extension' => $extension,
+ ];
+ }
+ }
+ }
+
+ // validate settings
+ if (isset($requirements['settings'])) {
+ foreach ($requirements['settings'] as $setting => $value) {
+ $iniValue = \ini_get($setting);
+
+ $passed = self::compareSetting($setting, $value, $iniValue);
+ if (!$passed) {
+ $errors['setting'][] = [
+ 'setting' => $setting,
+ 'required' => $value,
+ 'installed' => ($iniValue === false) ? '(unknown)' : $iniValue,
+ ];
+ }
+ }
+ }
+
+ // validate functions
+ if (isset($requirements['functions'])) {
+ foreach ($requirements['functions'] as $function) {
+ $function = \mb_strtolower($function);
+
+ $passed = self::functionExists($function);
+ if (!$passed) {
+ $errors['function'][] = [
+ 'function' => $function,
+ ];
+ }
+ }
+ }
+
+ // validate classes
+ if (isset($requirements['classes'])) {
+ foreach ($requirements['classes'] as $class) {
+ $passed = false;
+
+ // see: http://de.php.net/manual/en/language.oop5.basic.php
+ if (\preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*.~', $class)) {
+ $globalClass = '\\' . $class;
+
+ if (\class_exists($globalClass, false)) {
+ $passed = true;
+ }
+ }
+
+ if (!$passed) {
+ $errors['class'][] = [
+ 'class' => $class,
+ ];
+ }
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Validates if an function exists and is not blacklisted by suhosin extension.
+ *
+ * @param string $function
+ * @return bool
+ * @see http://de.php.net/manual/en/function.function-exists.php#77980
+ */
+ protected static function functionExists($function)
+ {
+ if (\extension_loaded('suhosin')) {
+ $blacklist = @\ini_get('suhosin.executor.func.blacklist');
+ if (!empty($blacklist)) {
+ $blacklist = \explode(',', $blacklist);
+ foreach ($blacklist as $disabledFunction) {
+ $disabledFunction = \mb_strtolower(StringUtil::trim($disabledFunction));
+
+ if ($function == $disabledFunction) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return \function_exists($function);
+ }
+
+ /**
+ * Compares settings, converting values into compareable ones.
+ *
+ * @param string $setting
+ * @param string $value
+ * @param mixed $compareValue
+ * @return bool
+ */
+ protected static function compareSetting($setting, $value, $compareValue)
+ {
+ if ($compareValue === false) {
+ return false;
+ }
+
+ $value = \mb_strtolower($value);
+ $trueValues = ['1', 'on', 'true'];
+ $falseValues = ['0', 'off', 'false'];
+
+ // handle values considered as 'true'
+ if (\in_array($value, $trueValues)) {
+ return $compareValue ? true : false;
+ } // handle values considered as 'false'
+ elseif (\in_array($value, $falseValues)) {
+ return (!$compareValue) ? true : false;
+ } elseif (!\is_numeric($value)) {
+ $compareValue = self::convertShorthandByteValue($compareValue);
+ $value = self::convertShorthandByteValue($value);
+ }
+
+ return ($compareValue >= $value) ? true : false;
+ }
+
+ /**
+ * Converts shorthand byte values into an integer representing bytes.
+ *
+ * @param string $value
+ * @return int
+ * @see http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
+ */
+ protected static function convertShorthandByteValue($value)
+ {
+ // convert into bytes
+ $lastCharacter = \mb_substr($value, -1);
+ switch ($lastCharacter) {
+ // gigabytes
+ case 'g':
+ return (int)$value * 1073741824;
+ break;
+
+ // megabytes
+ case 'm':
+ return (int)$value * 1048576;
+ break;
+
+ // kilobytes
+ case 'k':
+ return (int)$value * 1024;
+ break;
+
+ default:
+ return $value;
+ break;
+ }
+ }
}