<?php
/*
* Copyright (C) 2017 Karmabunny Pty Ltd.
*
* This file is a part of SproutCMS.
*
* SproutCMS is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either
* version 2 of the License, or (at your option) any later version.
*
* For more information, visit <http://getsproutcms.com>.
*/
namespace Sprout\Helpers;
use Exception;
use InvalidArgumentException;
use Kohana;
use karmabunny\pdb\Exceptions\QueryException;
/**
* Provides functions for getting information about subsites
**/
class Subsites
{
private static $subsites = null;
private static $configs = null;
/**
* Loads all the subsites into memory, ready for further processing
**/
static private function loadSubsites()
{
if (self::$subsites != null) return;
$q = "SELECT id, cond_directory, cond_domain, content_id, name, code
FROM ~subsites
ORDER BY record_order";
try {
$result = Pdb::query($q, [], 'pdo');
} catch (QueryException $ex) {
// Assume DB has no tables; pretend there's a subsite
self::$subsites = [];
self::$subsites[1] = [
'id' => 1,
'cond_directory' => '',
'cond_domains' => [],
'content_id' => '',
'name' => 'Site with no DB',
'code' => 'default',
];
return;
}
foreach ($result as $sub) {
$subsites[$sub['id']] = $sub;
}
$result->closeCursor();
self::$subsites = $subsites;
}
/**
* Determines if multiple subsites can be accessed by the currently logged in administrator
*
* @return bool True if multiple subsites are available, false if they are not
**/
public static function hasMultiple()
{
self::loadSubsites();
AdminAuth::checkLogin();
$count = 0;
foreach (self::$subsites as $sub) {
if (AdminPerms::canAccessSubsite($sub['id'])) $count++;
}
return ($count > 1);
}
/**
* Outputs a select UL for choosing a subsite from within the admin area
*
* @param int $selected_id The currently selected subsite, if any
**/
public static function listSelector($selected_id = null)
{
self::loadSubsites();
AdminAuth::checkLogin();
echo '<form action="admin/set_active_subsite" method="post">';
echo Csrf::token();
echo "<div class=\"field-element field-element--white field-element--select\">";
echo "<div class=\"field-label -vis-hidden\">";
echo "<label for=\"subsite-selector\">Current site</label>";
echo "</div>";
echo "<div class=\"field-input\">";
echo "<select id=\"subsite-selector\" name=\"subsite\">";
foreach (self::$subsites as $site) {
if (AdminPerms::canAccessSubsite($site['id'])) {
$site_name_html = Enc::html($site['name']);
// It's a shared-content subsite, they can't be edited
if ($site['content_id'] != 0) {
$shared_name_html = 'another subsite';
foreach (self::$subsites as $ss) {
if ($ss['id'] == $site['content_id']) { $shared_name_html = Enc::html($ss['name']); break; }
}
echo "<option value=\"\">{$site_name_html} (content from {$shared_name_html})</option>\n";
continue;
}
// Regular subsite with on-state
if ($site['id'] == $selected_id) {
echo "<option value=\"\" selected>Editing {$site_name_html}</option>\n";
} else {
echo "<option value=\"{$site['id']}\">{$site_name_html}</option>\n";
}
}
}
echo "</select>";
echo "</div>";
echo "</div>";
echo '</form>';
}
/**
* Returns the id of the first subsite in the list (alphabetical) that the user is able to access.
* Returns null if there are no subsites the user can access.
**/
public static function getFirstAccessable()
{
self::loadSubsites();
foreach (self::$subsites as $sub) {
if (AdminPerms::canAccessSubsite($sub['id'])) return $sub['id'];
}
return null;
}
/**
* Returns the name of the specified subsite
**/
public static function getName($id)
{
self::loadSubsites();
if (!isset(self::$subsites[$id])) { throw new InvalidArgumentException("Subsite #{$id} not found");
}
return self::$subsites[$id]['name'];
}
/**
* Returns the name of the specified subsite
**/
public static function getCode($id)
{
self::loadSubsites();
if (!isset(self::$subsites[$id])) { throw new InvalidArgumentException("Subsite #{$id} not found");
}
return self::$subsites[$id]['code'];
}
/**
* For a given list of domains, determine the best match based on the current HTTP_HOST
*
* If there is an exact match, it is used
* If there is an ends-with match, it is used
* Failing all that, the first domain in the list is used
**/
static private function determineBestDomain($domains)
{
if (empty($_SERVER['HTTP_HOST'])) return $domains[0];
foreach ($domains as $d) {
if ($d === $_SERVER['HTTP_HOST']) return $d;
}
foreach ($domains as $d) {
if (strrpos($d, $_SERVER['HTTP_HOST']) === strlen($d) - strlen($_SERVER['HTTP_HOST'])) return $d; }
return $domains[0];
}
/**
* Returns the abs root (including protocol and server name) of the specified subsite
* Falls back to the current root if no abs root can be found in the database
* @param int $id The subsite ID, e.g. SubsiteSelector::$subsite_id
* @param string $protocol The protocol to use for the link. Specifying null (the default)
* will cause the protocol to be automatically determined based on the request.
* @return string An absolute URL including protocol
**/
public static function getAbsRoot($id, $protocol = null)
{
self::loadSubsites();
if (!isset(self::$subsites[$id])) { throw new InvalidArgumentException('Subsite not found');
}
if (count(self::$subsites[$id]['cond_domains'])) { $domain = self::determineBestDomain(self::$subsites[$id]['cond_domains']);
} else {
$domain = $_SERVER['HTTP_HOST'];
}
if (!$protocol) {
if (PHP_SAPI != 'cli') {
$protocol = Request::protocol();
} else {
$protocol = 'http';
}
}
if (!empty(self::$subsites[$id]['cond_directory'])) { $path = rtrim(self::$subsites[$id]['cond_directory'], '/') . '/'; } else {
$path = '';
}
return $protocol . '://' . $domain . Kohana::config('config.site_domain') . $path;
}
/**
* Returns the abs root (including protocol and server name) of the current admin subsite
**/
public static function getAbsRootAdmin()
{
AdminAuth::checkLogin();
return self::getAbsRoot(@$_SESSION['admin']['active_subsite']);
}
/**
* Get a config value for a given subsite
*
* Use this instead of Kohana::config, if you need to target a specific subsite.
* It always looks in the "sprout" config file, doesn't support other files.
*
* @param string $key Configuration key
* @param int $subsite_id Subsite to get config value for
* @return mixed Configuration value
**/
public static function getConfig($key, $subsite_id)
{
$code = self::getCode($subsite_id);
if (!$code) {
throw new Exception('Subsite #'. $subsite_id . ' does not have a valid code specified');
}
$configuration = self::loadConfig($code);
return @$configuration[$key];
}
/**
* Admin version of the getConfig function, uses the currently active subsite
**/
public static function getConfigAdmin($key)
{
AdminAuth::checkLogin();
return self::getConfig($key, @$_SESSION['admin']['active_subsite']);
}
/**
* Load the configuration for a subsite code.
* It will only be loaded once per request - it gets cached in a static var
*
* @param string $subsite_code The subsite code to load and return config for
* @param array Configuration for that subsite
**/
public static function loadConfig($subsite_code)
{
if (isset(self::$configs[$subsite_code])) { return self::$configs[$subsite_code];
}
if (! file_exists(DOCROOT
. 'skin/' . $subsite_code . '/')) { throw new Exception('Invalid subsite "' . $subsite_code . '"; skin directory not found.');
}
DOCROOT . 'skin/' . $subsite_code . '/config/sprout.php',
);
$configuration = array(); foreach ($files as $file) {
include $file;
}
}
}
self::$configs[$subsite_code] = $configuration;
return $configuration;
}
/**
* Gets the list of subsite codes which are available, by reading the appropriate directory
* @return array
*/
public static function getCodes()
{
$codes = [];
$skin_dir = DOCROOT . 'skin' . DIRECTORY_SEPARATOR;
foreach ($files as $file) {
if ($file[0] == '.') continue;
if ($file == 'unavailable') continue;
if (!is_dir($skin_dir . $file)) continue; $codes[$file] = $file;
}
return $codes;
}
}