Simplify the domain and landing page management
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / application / ApplicationHandler.class.php
1 <?php
2
3 namespace wcf\system\application;
4
5 use wcf\data\application\Application;
6 use wcf\data\application\ApplicationAction;
7 use wcf\data\application\ApplicationList;
8 use wcf\data\package\Package;
9 use wcf\data\package\PackageList;
10 use wcf\system\cache\builder\ApplicationCacheBuilder;
11 use wcf\system\request\RequestHandler;
12 use wcf\system\request\RouteHandler;
13 use wcf\system\SingletonFactory;
14 use wcf\system\WCF;
15 use wcf\util\ArrayUtil;
16 use wcf\util\FileUtil;
17 use wcf\util\StringUtil;
18 use wcf\util\Url;
19
20 /**
21 * Handles multi-application environments.
22 *
23 * @author Alexander Ebert
24 * @copyright 2001-2019 WoltLab GmbH
25 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
26 * @package WoltLabSuite\Core\System\Application
27 */
28 class ApplicationHandler extends SingletonFactory
29 {
30 /**
31 * application cache
32 * @var mixed[][]
33 */
34 protected $cache;
35
36 /**
37 * true for multi-domain setups
38 * @var bool
39 */
40 protected $isMultiDomain;
41
42 /**
43 * list of page URLs
44 * @var string[]
45 */
46 protected $pageURLs = [];
47
48 /**
49 * Initializes cache.
50 */
51 protected function init()
52 {
53 $this->cache = ApplicationCacheBuilder::getInstance()->getData();
54 }
55
56 /**
57 * Returns an application based upon it's abbreviation. Will return the
58 * primary application if the abbreviation is `wcf` or `null` if no such
59 * application exists.
60 *
61 * @param string $abbreviation package abbreviation, e.g. `wbb` for `com.woltlab.wbb`
62 * @return Application|null
63 */
64 public function getApplication($abbreviation)
65 {
66 if (isset($this->cache['abbreviation'][$abbreviation])) {
67 $packageID = $this->cache['abbreviation'][$abbreviation];
68
69 if (isset($this->cache['application'][$packageID])) {
70 return $this->cache['application'][$packageID];
71 }
72 }
73 }
74
75 /**
76 * Returns an application delivered by the package with the given id or `null`
77 * if no such application exists.
78 *
79 * @param int $packageID package id
80 * @return Application|null application object
81 * @since 3.0
82 */
83 public function getApplicationByID($packageID)
84 {
85 // work-around for update from 2.1 (out-dated cache)
86 if ($packageID == 1 && !isset($this->cache['application'][1])) {
87 $this->cache['application'][1] = new Application(1);
88 }
89 if (isset($this->cache['application'][$packageID])) {
90 return $this->cache['application'][$packageID];
91 }
92 }
93
94 /**
95 * Returns pseudo-application representing WCF used for special cases,
96 * e.g. cross-domain files requestable through the webserver.
97 *
98 * @return Application
99 * @deprecated 3.0 please use `getApplication()` instead
100 */
101 public function getWCF()
102 {
103 return $this->getApplicationByID(1);
104 }
105
106 /**
107 * Returns the currently active application.
108 *
109 * @return Application
110 */
111 public function getActiveApplication()
112 {
113 // work-around during WCFSetup
114 if (!PACKAGE_ID) {
115 $host = \str_replace(RouteHandler::getProtocol(), '', RouteHandler::getHost());
116 $documentRoot = FileUtil::addTrailingSlash(FileUtil::unifyDirSeparator(\realpath($_SERVER['DOCUMENT_ROOT'])));
117
118 // always use the core directory
119 if (empty($_POST['directories']) || empty($_POST['directories']['wcf'])) {
120 // within ACP
121 $_POST['directories'] = ['wcf' => $documentRoot . FileUtil::removeLeadingSlash(RouteHandler::getPath(['acp']))];
122 }
123
124 $path = FileUtil::addLeadingSlash(FileUtil::addTrailingSlash(FileUtil::unifyDirSeparator(FileUtil::getRelativePath(
125 $documentRoot,
126 $_POST['directories']['wcf']
127 ))));
128
129 return new Application(null, [
130 'domainName' => $host,
131 'domainPath' => $path,
132 'cookieDomain' => $host,
133 ]);
134 }
135
136 $request = RequestHandler::getInstance()->getActiveRequest();
137 if ($request !== null) {
138 $abbreviation = \substr($request->getClassName(), 0, \mb_strpos($request->getClassName(), '\\'));
139
140 return $this->getApplication($abbreviation);
141 }
142
143 if (isset($this->cache['application'][PACKAGE_ID])) {
144 return $this->cache['application'][PACKAGE_ID];
145 }
146
147 return $this->getWCF();
148 }
149
150 /**
151 * Returns a list of dependent applications.
152 *
153 * @return Application[]
154 */
155 public function getDependentApplications()
156 {
157 $applications = $this->getApplications();
158 foreach ($applications as $key => $application) {
159 if ($application->packageID == $this->getActiveApplication()->packageID) {
160 unset($applications[$key]);
161 break;
162 }
163 }
164
165 return $applications;
166 }
167
168 /**
169 * Returns a list of all active applications.
170 *
171 * @return Application[]
172 */
173 public function getApplications()
174 {
175 return $this->cache['application'];
176 }
177
178 /**
179 * Returns abbreviation for a given package id or `null` if application is unknown.
180 *
181 * @param int $packageID unique package id
182 * @return string|null
183 */
184 public function getAbbreviation($packageID)
185 {
186 foreach ($this->cache['abbreviation'] as $abbreviation => $applicationID) {
187 if ($packageID == $applicationID) {
188 return $abbreviation;
189 }
190 }
191 }
192
193 /**
194 * Returns the list of application abbreviations.
195 *
196 * @return string[]
197 * @since 3.1
198 */
199 public function getAbbreviations()
200 {
201 return \array_keys($this->cache['abbreviation']);
202 }
203
204 /**
205 * Returns true if given $url is an internal URL.
206 *
207 * @param string $url
208 * @return bool
209 */
210 public function isInternalURL($url)
211 {
212 if (empty($this->pageURLs)) {
213 foreach ($this->getApplications() as $application) {
214 $this->pageURLs[] = $application->domainName;
215 }
216
217 $this->pageURLs = \array_unique(\array_merge(
218 $this->pageURLs,
219 ArrayUtil::trim(\explode("\n", StringUtil::unifyNewlines(\INTERNAL_HOSTNAMES)))
220 ));
221 }
222
223 $host = Url::parse($url)['host'];
224
225 // Relative URLs are internal.
226 if (!$host) {
227 return true;
228 }
229
230 return Url::getHostnameMatcher($this->pageURLs)($host);
231 }
232
233 /**
234 * Returns true if this is a multi-domain setup.
235 *
236 * @return bool
237 * @since 3.1
238 * @deprecated 5.4
239 */
240 public function isMultiDomainSetup()
241 {
242 if ($this->isMultiDomain === null) {
243 $this->isMultiDomain = false;
244
245 $domainName = $this->getApplicationByID(1)->domainName;
246 foreach ($this->getApplications() as $application) {
247 if ($application->domainName !== $domainName) {
248 $this->isMultiDomain = true;
249 break;
250 }
251 }
252 }
253
254 return $this->isMultiDomain;
255 }
256
257 /**
258 * @since 5.2
259 */
260 public function rebuildActiveApplication()
261 {
262 /** @var AbstractApplication $application */
263 foreach ($this->cache['application'] as $application) {
264 if ($application->getPackage()->package === 'com.woltlab.wcf') {
265 continue;
266 }
267
268 $appObject = WCF::getApplicationObject($application);
269 if ($appObject instanceof AbstractApplication) {
270 $appObject->rebuildActiveApplication();
271 }
272 }
273 }
274
275 /**
276 * Rebuilds cookie domain/path for all applications.
277 */
278 public static function rebuild()
279 {
280 $applicationList = new ApplicationList();
281 $applicationList->readObjects();
282
283 $applicationAction = new ApplicationAction($applicationList->getObjects(), 'rebuild');
284 $applicationAction->executeAction();
285 }
286
287 /**
288 * Replaces `app1_` in the given string with the correct installation number:
289 * `app{WCF_N_}`.
290 *
291 * This method can either be used for database table names directly or for
292 * queries, for example.
293 *
294 * @param string $string string to be processed
295 * @param bool $skipCache if `true`, no caches will be used and relevant application packages will be read from database directly
296 * @return string processed string
297 * @since 5.2
298 */
299 public static function insertRealDatabaseTableNames($string, $skipCache = false)
300 {
301 if ($skipCache) {
302 $packageList = new PackageList();
303 $packageList->getConditionBuilder()->add('package.isApplication = ?', [1]);
304 $packageList->readObjects();
305
306 foreach ($packageList as $package) {
307 $abbreviation = Package::getAbbreviation($package->package);
308
309 $string = \str_replace($abbreviation . '1_', $abbreviation . WCF_N . '_', $string);
310 }
311 } else {
312 foreach (static::getInstance()->getAbbreviations() as $abbreviation) {
313 $string = \str_replace($abbreviation . '1_', $abbreviation . WCF_N . '_', $string);
314 }
315 }
316
317 return $string;
318 }
319 }