PHP Classes
elePHPant
Icontem

File: src/view_functions.php

Recommend this page to a friend!
  Classes of Scott Arciszewski  >  CMS Airship  >  src/view_functions.php  >  Download  
File: src/view_functions.php
Role: Example script
Content type: text/plain
Description: Example script
Class: CMS Airship
Content management system with security features
Author: By
Last change: If currently using HTTPS, force canon_url to use HTTPS.
Date: 28 days ago
Size: 20,336 bytes
 

Contents

Class file image Download
<?php
declare(strict_types=1);
namespace Airship\ViewFunctions;

use Airship\Engine\{
    AutoPilot, Cache\ViewCache, Gadgets, Gears, Security\CSRF, Security\Util, State
};
use Airship\Engine\Security\Permissions;
use ParagonIE\CSPBuilder\CSPBuilder;
use ParagonIE\ConstantTime\{
    Base64,
    Base64UrlSafe,
    Hex
};
use ParagonIE\Halite\{
    Asymmetric\SignaturePublicKey,
    Asymmetric\SignatureSecretKey,
    File,
    HiddenString,
    Symmetric\AuthenticationKey,
    Util as CryptoUtil
};

/**
 * Return a reusable CSRF prevention token, for AJAX requests.
 *
 * @param string $lockTo
 * @return string
 * @throws \TypeError
 */
function ajax_token($lockTo = '')
{
    static $csrf = null;
    if ($csrf === null) {
        /** @var CSRF $csrf */
        $csrf = Gears::get('CSRF');
    }
    if (!($csrf instanceof CSRF)) {
        throw new \TypeError('Incorrect type for CSRF class');
    }
    return $csrf->ajaxToken($lockTo);
}

/**
 * Get the base template (normally "base.twig")
 *
 * @return string
 */
function base_template()
{
    $state = State::instance();
    return $state->base_template;
}

/**
 * READ-ONLY access to the state global
 *
 * @param string $name
 * @return array
 */
function cabin_config(string $name = \CABIN_NAME): array
{
    $state = State::instance();
    foreach ($state->cabins as $route => $cabin) {
        if ($cabin['name'] === $name) {
            return $cabin;
        }
    }
    return [];
}

/**
 * READ-ONLY access to the cabin settings
 *
 * @param string $name
 * @return array
 */
function cabin_custom_config(string $name = \CABIN_NAME): array
{
    return \Airship\loadJSON(
        ROOT . '/Cabin/' . $name . '/config/config.json'
    );
}

/**
 * Get the canon URL for a given Cabin
 *
 * @param string $cabin
 * @return string
 *
 * @throws \Exception
 */
function cabin_url(string $cabin = \CABIN_NAME): string
{
    static $lookup = [];
    $noArgs = \func_num_args() === 0;
    if (!empty($lookup[$cabin])) {
        // It was cached
        return $lookup[$cabin];
    }
    $state = State::instance();
    foreach ($state->cabins as $c) {
        if ($c['name'] === $cabin) {
            if (isset($c['canon_url'])) {
                $lookup[$cabin] = \rtrim($c['canon_url'], '/') . '/';
                if (AutoPilot::isHTTPSConnection() && $cabin === \CABIN_NAME) {
                    $lookup[$cabin] = \Airship\makeHttps($lookup[$cabin]);
                }
                return $lookup[$cabin];
            }
            $lookup[$cabin] = '/';
            return $lookup[$cabin];
        }
    }
    return '';
}

/**
 * Used in our cachebust filter. This is mostly useful for HTML5 app caching
 *
 * @param $relative_path
 * @return string
 */
function cachebust($relative_path)
{
    if ($relative_path[0] !== '/') {
        $relative_path = '/' . $relative_path;
    }
    $absolute = $_SERVER['DOCUMENT_ROOT'] . $relative_path;
    if (\is_readable($absolute)) {
        // Halite's File::checksum() uses less memory than reading the entire
        // file into memory.
        $key = new AuthenticationKey(
            new HiddenString(
                CryptoUtil::raw_hash(
                    (string) \filemtime($absolute)
                )
            )
        );
        return $relative_path . '?' . Base64UrlSafe::encode(
            File::checksum(
                $absolute,
                $key,
                true
            )
        );
    }
    // Special value
    return $relative_path . '?404NotFound';
}

/**
 * Permission Look-Up
 *
 * @param string $label
 * @param string $context_regex
 * @param string $domain
 * @param int $user_id
 * @return bool
 * @throws \Airship\Alerts\Database\DBException
 */
function can(
    string $label,
    string $context_regex = '',
    string $domain = \CABIN_NAME,
    int $user_id = 0
): bool {
    static $perm = null;
    if ($perm === null) {
        $perm = Gears::get(
            'Permissions',
            \Airship\get_database()
        );
    }
    if (!($perm instanceof Permissions)) {
        return false;
    }
    return $perm->can($label, $context_regex, $domain, $user_id);
}

/**
 * Wrapper for `Gadgets::unloadCargo()`
 *
 * @param string $name
 * @param int $offset
 * @return array
 */
function cargo(string $name, int $offset = 0): array
{
    return Gadgets::unloadCargo($name, $offset);
}

/**
 * Hash a file and store its hash in the Content-Security-Policy header
 *
 * @param string $file
 * @param string $dir
 * @param string $algo
 * @return string
 */
function csp_hash(
    string $file,
    string $dir = 'script-src',
    string $algo = 'sha384'
): string {
    $state = State::instance();
    if (isset($state->CSP)) {

        list ($dirName, $lastPiece) = ViewCache::getFile(
            'Content Security Policy Hash:',
            'csp_hash',
            $file
        );

        $fileName = $dirName . '/' . $lastPiece;

        if (\file_exists($fileName)) {
            if ($state->CSP instanceof CSPBuilder) {
                $prehash = \file_get_contents($fileName);
                if (!\is_string($prehash)) {
                    // Network connection errors
                    return $file;
                }
                $state->CSP->preHash(
                    $dir,
                    $prehash,
                    $algo
                );
            }
            return $file;
        }

        // Cache miss.
        if (\preg_match('#^([A-Za-z]+):\/\/#', $file)) {
            $absolute = $file;
        } else {
            if ($file[0] !== '/') {
                $file = '/' . $file;
            }
            $absolute = $_SERVER['DOCUMENT_ROOT'] . $file;
            if (!\file_exists($absolute)) {
                return $file;
            }
        }

        if ($state->CSP instanceof CSPBuilder) {
            $contents = \file_get_contents($absolute);
            if (!\is_string($contents)) {
                // Network connection errors
                return $file;
            }
            $preHash = Base64::encode(
                \hash($algo, \file_get_contents($absolute), true)
            );
            $state->CSP->preHash($dir, $preHash, $algo);

            if (!\is_dir($dirName)) {
                \mkdir($dirName, 0775, true);
            }

            \file_put_contents(
                $fileName,
                $preHash
            );
            return $file;
        }
    }
    return $file;
}

/**
 * Hash a string and store its hash in the Content-Security-Policy header
 *
 * @param string $str   The data we are hashing
 * @param string $dir   The CSP Directive
 * @param string $algo  Which hash algorithm?
 * @return string       $str
 */
function csp_hash_str(
    string $str,
    string $dir = 'script-src',
    string $algo = 'sha384'
): string {
    $state = State::instance();
    if (isset($state->CSP)) {
        if ($state->CSP instanceof CSPBuilder) {
            $preHash = \hash($algo, $str, true);
            $state->CSP->preHash(
                $dir,
                Base64::encode($preHash),
                $algo
            );
            return $str;
        }
    }
    return $str;
}

/**
 * Generate a nonce, add to the CSP header
 *
 * @param string $dir
 * @return string
 */
function csp_nonce(string $dir = 'script-src'): string
{
    $state = State::instance();
    if (isset($state->CSP)) {
        if ($state->CSP instanceof CSPBuilder) {
            return (string) $state->CSP->nonce($dir);
        }
    }
    return 'noCSPInstalled';
}

/**
 * Insert a CSRF prevention token
 *
 * @param string $lockTo
 * @return string
 * @throws \TypeError
 */
function form_token($lockTo = '')
{
    static $csrf = null;
    if ($csrf === null) {
        /** @var CSRF $csrf */
        $csrf = Gears::get('CSRF');
        if (!($csrf instanceof CSRF)) {
            throw new \TypeError('Incorrect type for CSRF class');
        }
    }
    return $csrf->insertToken($lockTo);
}

/**
 * Given a URL, only grab the path component (and, optionally, the query)
 *
 * @param string $url
 * @param bool $includeQuery
 * @return string
 */
function get_path_url(string $url, bool $includeQuery = false): string
{
    $path = \parse_url($url, PHP_URL_PATH);
    if ($path) {
        if ($includeQuery) {
            $query = \parse_url($url, PHP_URL_QUERY);
            if ($query) {
                return $url . '?' . $query;
            }
        }
        return $path;
    }
    return '';
}

/**
 * Display the notary <meta> tag.
 *
 * @param SignaturePublicKey $pk
 */
function display_notary_tag(SignaturePublicKey $pk = null)
{
    $state = State::instance();
    $notary = $state->universal['notary'];
    if (!empty($notary['enabled'])) {
        if (!$pk) {
            $sk = $state->keyring['notary.online_signing_key'];
            if (!($sk instanceof SignatureSecretKey)) {
                return;
            }
            $pk = $sk
                ->derivePublicKey()
                ->getRawKeyMaterial();
        }
        echo '<meta name="airship-notary" content="' . 
                Base64UrlSafe::encode($pk) .
            '; channel=' . Util::noHTML($notary['channel']) .
            '; url=' . cabin_url('Bridge') . 'notary' .
        '" />';
    }
}

/**
 * Get supported languages. Eventually there will be more than one.
 */
function get_languages(): array
{
    return [
        'en-us' => 'English (U.S.)'
    ];
}

/**
 * Get an author profile's avatar
 *
 * @param int $authorId
 * @param string $which
 * @return string
 */
function get_avatar(int $authorId, string $which): string
{
    static $cache = [];
    static $db = null;
    if (!$db) {
        $db = \Airship\get_database();
    }
    // If someone comments 100 times, we only want to look up their avatar once.
    $key = CryptoUtil::hash(
        \http_build_query([
            'author' => $authorId,
            'which' => $which
        ])
    );
    if (!isset($cache[$key])) {
        $file = $db->row(
            "SELECT
                 f.*,
                 a.slug
             FROM
                 hull_blog_author_photos p
             JOIN
                 hull_blog_authors a
                 ON p.author = a.authorid
             JOIN
                 hull_blog_photo_contexts c
                 ON p.context = c.contextid
             JOIN
                 airship_files f
                 ON p.file = f.fileid
             WHERE
                 c.label = ? AND a.authorid = ?
             ",
            $which,
            $authorId
        );
        if (empty($file)) {
            $cache[$key] = '';
        } else {
            if (empty($file['directory'])) {
                $cabin = $file['cabin'];
            }  else {
                $dirId = $file['directory'];
                do {
                    $dir = $db->row(
                        "SELECT parent, cabin FROM airship_dirs WHERE directoryid = ?",
                        $dirId
                    );
                    $dirId = $dir['parent'];
                } while (!empty($dirId));
                $cabin = $dir['cabin'];
            }
            $cache[$key] = \Airship\ViewFunctions\cabin_url($cabin) .
                'files/author/' .
                $file['slug'] .
                '/photos/' .
                $file['filename'];
        }
    }

    return $cache[$key];
}

/**
 * Purify a string using HTMLPurifier
 *
 * @param $string
 * @return string
 * @throws \TypeError
 */
function get_purified(string $string = '')
{
    $gear = get_viewcache_obj();
    return $gear::purify($string);
}

/**
 * @return ViewCache
 * @throws \TypeError
 */
function get_viewcache_obj(): ViewCache
{
    static $gear = null;
    if (!$gear) {
        /**
         * @var ViewCache
         */
        $gear = Gears::get('ViewCache');
        if (!$gear instanceof ViewCache) {
            throw new \TypeError();
        }
    }
    return $gear;
}

/**
 * READ-ONLY access to the state global
 *
 * @param string $key
 * @return array
 */
function global_config(string $key): array
{
    $state = State::instance();
    switch ($key) {
        case 'active_cabin':
            return [
                $state->{$key}
            ];
        case 'base_template':
        case 'cabins':
        case 'cargo':
        case 'motifs':
        case 'gears':
        case 'lang':
        case 'universal':
            return $state->{$key};
        default:
            return [];
    }
}

/**
 * Is this user an administrator?
 *
 * @param int $userID
 * @return bool
 * @throws \Airship\Alerts\Database\DBException
 */
function is_admin(int $userID = 0): bool
{
    static $perm = null;
    if ($perm === null) {
        $perm = Gears::get(
            'Permissions',
            \Airship\get_database()
        );
        if (!($perm instanceof Permissions)) {
            return false;
        }
    }
    if ($userID < 1) {
        $userID = \Airship\ViewFunctions\userid();
    }
    return $perm->isSuperUser($userID);
}

/**
 * Json_encode and Echo
 *
 * @param mixed $data
 * @param int $indents
 */
function je($data, int $indents = 0)
{
    if ($indents > 0) {
        $left = \str_repeat('    ', $indents);
        echo \implode(
            "\n" . $left,
            \explode(
                "\n",
                \json_encode($data, JSON_PRETTY_PRINT)
            )
        );
        return;
    }
    echo \json_encode($data, JSON_PRETTY_PRINT);
}

/**
 * Return the user's logout token. This is to prevent logout via CSRF.
 *
 * @return string
 */
function logout_token(): string
{
    if (\array_key_exists('logout_token', $_SESSION)) {
        return $_SESSION['logout_token'];
    }
    $_SESSION['logout_token'] = Hex::encode(
        \random_bytes(16)
    );
    return $_SESSION['logout_token'];
}

/**
 * Get information about the motifs
 *
 * @return array
 */
function motifs()
{
    $state = State::instance();
    return $state->motifs;
}

/**
 * Unload the "next" cargo, using an internal iterator.
 *
 * @param string $cargoName
 * @return array
 */
function next_cargo(string $cargoName)
{
    return Gadgets::unloadNextCargo($cargoName);
}

/**
 * Render user input, with CommonMark.
 *
 * @param string $string
 * @param bool $return
 * @output HTML
 * @return string
 */
function render_markdown(string $string = '', bool $return = false): string
{
    $gear = get_viewcache_obj();
    return $gear::markdown($string, $return);
}

/**
 * Renders ReStructuredText
 *
 * @param string $string
 * @param bool $return
 * @output HTML
 * @return string
 */
function render_rst(string $string = '', bool $return = false): string
{
    $gear = get_viewcache_obj();
    return $gear::rst($string, $return);
}

/**
 * Purify a string using HTMLPurifier. Echo its contents.
 *
 * @param $string
 * @return void
 */
function purify(string $string = '')
{
    echo get_purified($string);
}

/**
 * Markdown then HTMLPurifier
 *
 * @param string $string
 * @param bool $return
 * @return string|null
 */
function render_purified_markdown(string $string = '', bool $return = false)
{
    if ($return) {
        \ob_start();
    }
    \Airship\ViewFunctions\purify(
        \Airship\ViewFunctions\render_markdown($string, true)
    );
    if ($return) {
        return \ob_get_clean();
    }
    return null;
}

/**
 * ReStructuredText then HTMLPurifier
 *
 * @param string $string
 * @param bool $return
 * @return string|null
 */
function render_purified_rest(string $string = '', bool $return = false)
{
    if ($return) {
        \ob_start();
    }
    \Airship\ViewFunctions\purify(
        \Airship\ViewFunctions\render_rst($string, true)
    );
    if ($return) {
        return \ob_get_clean();
    }
    return null;
}

/**
 * Get the current user's ID. Returns 0 if not logged in.
 *
 * @return int
 */
function userid(): int
{
    return \array_key_exists('userid', $_SESSION)
        ? (int) $_SESSION['userid']
        : 0;
}

/**
 * Get all of a user's author profiles
 *
 * @param int|null $userId
 * @return array
 * @throws \Airship\Alerts\Database\DBException
 */
function user_authors(int $userId = null): array
{
    if (empty($userId)) {
        $userId = \Airship\ViewFunctions\userid();
    }
    $db = \Airship\get_database();
    $authors = $db->run(
        'SELECT * FROM view_hull_users_authors WHERE userid = ?',
        $userId
    );
    if (empty($authors)) {
        return [];
    }
    return $authors;
}

/**
 * Get all of a user's author profiles
 *
 * @param int|null $userId
 * @return array
 * @throws \Airship\Alerts\Database\DBException
 */
function user_author_ids(int $userId = null): array
{
    if (empty($userId)) {
        $userId = \Airship\ViewFunctions\userid();
    }
    $db = \Airship\get_database();
    $authors = $db->first(
        'SELECT authorid FROM hull_blog_author_owners WHERE userid = ?',
        $userId
    );
    if (empty($authors)) {
        return [];
    }
    return $authors;
}

/**
 * Get the user's public display name.
 *
 * @param int|null $userId
 * @return string
 * @throws \Airship\Alerts\Database\DBException
 */
function user_display_name(int $userId = null): string
{
    if (empty($userId)) {
        $userId = \Airship\ViewFunctions\userid();
    }
    $db = \Airship\get_database();
    $displayName = $db->cell(
        "SELECT
             COALESCE(
                 display_name,
                 real_name,
                 username
             )
         FROM
             airship_users
         WHERE
             userid = ?
         ",
        $userId
    );
    if (empty($displayName)) {
        return '';
    }
    return get_purified($displayName);
}

/**
 * Get the user's selected Motif
 *
 * @param int|null $userId
 * @param string $cabin
 * @return array
 */
function user_motif(int $userId = null, string $cabin = \CABIN_NAME): array
{
    static $userCache = [];
    $state = State::instance();

    if (\count($state->motifs) === 0) {
        return [];
    }
    if (empty($userId)) {
        $userId = \Airship\ViewFunctions\userid();
        if (empty($userId)) {
            $k = \array_keys($state->motifs)[0];
            return $state->motifs[$k] ?? [];
        }
    }
    // Did we cache these preferences?
    if (isset($userCache[$userId])) {
        return $state->motifs[$userCache[$userId]];
    }

    $db = \Airship\get_database();

    $userPrefs = $db->cell(
        'SELECT preferences FROM airship_user_preferences WHERE userid = ?',
        $userId
    );

    if (empty($userPrefs)) {
        // Default
        $k = \array_keys($state->motifs)[0];
        $userCache[$userId] = $k;
        return $state->motifs[$k] ?? [];
    }

    $userPrefs = \Airship\parseJSON($userPrefs, true);
    if (isset($userPrefs['motif'][$cabin])) {
        $split = \explode('/', $userPrefs['motif'][$cabin]);
        foreach ($state->motifs as $k => $motif) {
            if (empty($motif['config'])) {
                continue;
            }
            if (
                $motif['supplier'] === $split[0]
                    &&
                $motif['name'] === $split[1]
            ) {
                // We've found a match:
                $userCache[$userId] = $k;
                return $state->motifs[$k];
            }
        }
    }

    // When all else fails, go with the first one
    $k = \array_keys($state->motifs)[0];
    $userCache[$userId] = $k;
    return $state->motifs[$k] ?? [];
}

/**
 * Get a user's username, given their user ID
 *
 * @param int|null $userId
 * @return string
 * @throws \Airship\Alerts\Database\DBException
 */
function user_name(int $userId = null): string
{
    if (empty($userId)) {
        $userId = \Airship\ViewFunctions\userid();
    }
    $db = \Airship\get_database();
    return get_purified(
        $db->cell(
            'SELECT username FROM airship_users WHERE userid = ?',
            $userId
        )
    );
}

/**
 * Get the user's public display name.
 *
 * @param int|null $userId
 * @return string
 * @throws \Airship\Alerts\Database\DBException
 */
function user_unique_id(int $userId = null): string
{
    if (empty($userId)) {
        $userId = \Airship\ViewFunctions\userid();
    }
    $db = \Airship\get_database();
    return get_purified(
        $db->cell(
            'SELECT uniqueid FROM airship_users WHERE userid = ?',
            $userId
        )
    );
}