haxtheweb / issues

Issue queue for hax, haxcms, elmsln, lrnwebcomponents, wcfactory, websites and more.
Apache License 2.0
10 stars 1 forks source link

Make HAXsite stand alone even in PHP environments #1893

Open btopro opened 5 months ago

btopro commented 5 months ago

We need a way of shipping settings and awareness in index.php that..

Lesson learned from nodejs version

btopro commented 5 months ago
<?php
$bootstrapPath = "/../..";
if (file_exists("/.dockerenv") && __DIR__ == "/var/www/html") {
  $GLOBALS["HAXcmsInDocker"] = true;
  $bootstrapPath = "";
}
/*if (file_exists(__DIR__ . $bootstrapPath . '/system/backend/php/bootstrapHAX.php')) {
  include_once __DIR__ . $bootstrapPath . '/system/backend/php/bootstrapHAX.php';
  include_once $GLOBALS['HAXCMS']->configDirectory . '/config.php';
}*/
// local slim config loader off of site.json
if (!isset($GLOBALS['HAXCMS'])) {
  /**
   * A slim version of HAXcms that is just config driven off site.json
   */
  define("HAXCMS_FALLBACK_HEX", "#3f51b5");
  class HAXSiteConfig {
    public function __construct($location) {
      $this->basePath = "/";
      $this->cdn = './';
      $this->developerMode = FALSE;
      $this->developerModeAdminOnly = FALSE;
      $this->sitesDirectory = 'sites';
      $this->color = "blue";
      $this->manifest = new stdClass();
      $this->manifest->items = array();
      // ensure we have site.json to load config from
      if (file_exists($location)) {
        $this->file = $location;
        $fileData = json_decode(file_get_contents($location));
        $vars = get_object_vars($fileData);
        foreach ($vars as $key => $var) {
            if ($key != 'items') {
                $this->manifest->{$key} = $var;
            }
        }
        // also ensures data matches only what is supported
        if (isset($vars['items'])) {
            foreach ($vars['items'] as $key => $item) {
                $newItem = new stdClass();
                $newItem->id = $item->id;
                $newItem->indent = $item->indent;
                $newItem->location = $item->location;
                $newItem->slug = (isset($item->slug) ? $item->slug : str_replace('pages/', '', str_replace('/index.html','', $item->location)));
                $newItem->order = $item->order;
                $newItem->parent = $item->parent;
                $newItem->title = $item->title;
                $newItem->description = $item->description;
                // metadata can be anything so whatever
                $newItem->metadata = $item->metadata;
                $this->manifest->items[$key] = $newItem;
            }
        }
        $this->page = $this->loadNodeByLocation();
        if (isset($this->manifest->metadata->theme->variables->cssVariable)) {
          $this->color = 'var(' . $this->manifest->metadata->theme->variables->cssVariable . ', #FF2222)';
        }
        $this->name = $this->manifest->metadata->site->name;
      }
    }
    /**
     * Return the base tag accurately which helps with the PWA / SW side of things
     * @return string HTML blob for hte <base> tag
     */
    public function getBaseTag() {
        return '<base href="' . $this->basePath . '" />';
    }
    public function cacheBusterHash() {
      return '?';
    }
    /**
     * Return attributes for the site
     * @todo make this mirror the drupal get attributes method
     * @return string eventually, array of data keyed by type of information
     */
    public function getSitePageAttributes() {
        return 'vocab="http://schema.org/" prefix="oer:http://oerschema.org cc:http://creativecommons.org/ns dc:http://purl.org/dc/terms/"';
    }
    public function getLanguage() {
        if (isset($this->manifest->metadata->site->settings->lang) && $this->manifest->metadata->site->settings->lang != "" && $this->manifest->metadata->site->settings->lang != null) {
        return $this->manifest->metadata->site->settings->lang;
        }
        return "en-US";
    }

    /**
     * Return the active URI if it exists
     */
    public function getURI() {
      if (isset($_SERVER['SCRIPT_URI'])) {
        return $_SERVER['SCRIPT_URI'];
      }
    }
    /**
     * Return the active domain if it exists
     */
    public function getDomain() {
      if (isset($_SERVER['SERVER_NAME'])) {
        return 'https://' . $_SERVER['SERVER_NAME'];
      }
    }
    /**
     * Return accurate, rendered site metadata
     * @return string an html chunk of tags for the head section
     * @todo move this to a render function / section / engine
     */
    public function getSiteMetadata($page = NULL, $domain = NULL, $cdn = '') {
      $preloadTags = array();
        $content = $this->getPageContent($page);
        preg_match_all("/<(?:\"-[^\"]*\"['\"]*|'[^']*'['\"]*|[^'\">])+>/", $content, $matches);
        foreach ($matches[0] as $match) {
          if (strpos($match, '-')) {
            $tag = str_replace('>', '', str_replace('</', '', $match));
            $preloadTags[$tag] = $tag;
          }
        }
      // domain's need to inject their own full path for OG metadata (which is edge case)
      // most of the time this is the actual usecase so use the active path
      if (is_null($domain)) {
        $domain = $this->getURI();
      }
      // support preconnecting CDNs, sets us up for dynamic CDN switching too
      $preconnect = '';
      $base = './';
      if ($cdn == '' && $this->cdn != './') {
        $cdn = $this->cdn;
      }
      if ($cdn != '') {
        // preconnect for faster DNS lookup
        $preconnect = '<link rel="preconnect" crossorigin href="' . $cdn . '">';
        // base is preload for the calls below
        $base = $cdn;
      }
      $contentPreload = '';
      $wcMap = $this->getWCRegistryJson($base);
      foreach ($preloadTags as $tag) {
        // means the tag is known in our registry
        if (isset($wcMap->{$tag})) {
          $contentPreload .= '
          <link rel="preload" href="' . $base . 'build/es6/node_modules/' . $wcMap->{$tag} . '" as="script" crossorigin="anonymous" />
          <link rel="modulepreload" href="' . $base . 'build/es6/node_modules/' . $wcMap->{$tag} . '" />';
                }
              }
              $title = $page->title;
              $siteTitle = $this->manifest->title . ' | ' . $page->title;
              $description = $page->description;
              $hexCode = HAXCMS_FALLBACK_HEX;
              $themePreload = '';
              // sanity check, then preload the theme
              if (isset($this->manifest->metadata->theme->path)) {
                $themePreload = '  <link rel="preload" href="' . $base . 'build/es6/node_modules/' . $this->manifest->metadata->theme->path . '" as="script" crossorigin="anonymous" />
          <link rel="modulepreload" href="' . $base . 'build/es6/node_modules/' . $this->manifest->metadata->theme->path . '" />';
              }
              if ($description == '') {
                $description = $this->manifest->description;
              }
              if ($title == '' || $title == 'New item') {
                $title = $this->manifest->title;
                $siteTitle = $this->manifest->title;
              }
              if (isset($this->manifest->metadata->theme->variables->hexCode)) {
                  $hexCode = $this->manifest->metadata->theme->variables->hexCode;
              }
              $metadata = '
          <meta charset="utf-8">' . $preconnect . '
          <link rel="preconnect" crossorigin href="https://fonts.googleapis.com">
          <link rel="preconnect" crossorigin href="https://cdnjs.cloudflare.com">
          <link rel="preload" href="' . $base . 'build.js" as="script" />
          <link rel="preload" href="' . $base . 'build-haxcms.js" as="script" />
          <link rel="preload" href="' . $base . 'wc-registry.json" as="fetch" crossorigin="anonymous" />
          <link rel="preload" href="' . $base . 'build/es6/node_modules/@lrnwebcomponents/dynamic-import-registry/dynamic-import-registry.js" as="script" crossorigin="anonymous" />
          <link rel="modulepreload" href="' . $base . 'build/es6/node_modules/@lrnwebcomponents/dynamic-import-registry/dynamic-import-registry.js" />
          <link rel="preload" href="' . $base . 'build/es6/node_modules/@lrnwebcomponents/wc-autoload/wc-autoload.js" as="script" crossorigin="anonymous" />
          <link rel="modulepreload" href="' . $base . 'build/es6/node_modules/@lrnwebcomponents/wc-autoload/wc-autoload.js" />
      ' . $themePreload . $contentPreload . '
        <link rel="preload" href="' . $base . 'build/es6/node_modules/@lrnwebcomponents/haxcms-elements/lib/base.css" as="style" />
        <meta name="generator" content="HAXcms">
        <link rel="manifest" href="manifest.json">
        <meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">
        <title>' . $siteTitle . '</title>
        <link rel="icon" href="' . $this->getLogoSize('16', '16') . '">
        <meta name="theme-color" content="' . $hexCode . '">
        <meta name="robots" content="index, follow">
        <meta name="mobile-web-app-capable" content="yes">
        <meta name="application-name" content="' . $title . '">
        <meta name="apple-mobile-web-app-capable" content="yes">
        <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
        <meta name="apple-mobile-web-app-title" content="' . $title . '">
        <link rel="apple-touch-icon" sizes="48x48" href="' . $this->getLogoSize('48', '48') . '">
        <link rel="apple-touch-icon" sizes="72x72" href="' . $this->getLogoSize('72', '72') . '">
        <link rel="apple-touch-icon" sizes="96x96" href="' . $this->getLogoSize('96', '96') . '">
        <link rel="apple-touch-icon" sizes="144x144" href="' . $this->getLogoSize('144', '144') . '">
        <link rel="apple-touch-icon" sizes="192x192" href="' . $this->getLogoSize('192', '192') . '">
        <meta name="msapplication-TileImage" content="' . $this->getLogoSize('144', '144') . '">
        <meta name="msapplication-TileColor" content="' . $hexCode . '">
        <meta name="msapplication-tap-highlight" content="no">
        <meta name="description" content="' . $description . '" />
        <meta name="og:sitename" property="og:sitename" content="' . $this->manifest->title . '" />
        <meta name="og:title" property="og:title" content="' . $title . '" />
        <meta name="og:type" property="og:type" content="article" />
        <meta name="og:url" property="og:url" content="' . $domain . '" />
        <meta name="og:description" property="og:description" content="' . $description . '" />
        <meta name="og:image" property="og:image" content="' . $this->getSocialShareImage($page) . '" />
        <meta name="twitter:card" property="twitter:card" content="summary_large_image" />
        <meta name="twitter:site" property="twitter:site" content="' . $domain . '" />
        <meta name="twitter:title" property="twitter:title" content="' . $title . '" />
        <meta name="twitter:description" property="twitter:description" content="' . $description . '" />
        <meta name="twitter:image" property="twitter:image" content="' . $this->getSocialShareImage($page) . '" />';  
      // mix in license metadata if we have it
      $licenseData = $this->getLicenseData('all');
      if (isset($this->manifest->license) && isset($licenseData[$this->manifest->license])) {
          $metadata .= "\n" . '  <meta rel="cc:license" href="' . $licenseData[$this->manifest->license]['link'] . '" content="License: ' . $licenseData[$this->manifest->license]['name'] . '"/>' . "\n";
      }
      // add in twitter link if they provided one
      if (isset($this->manifest->metadata->author->socialLink) && strpos($this->manifest->metadata->author->socialLink, 'https://twitter.com/') === 0) {
          $metadata .= '  <meta name="twitter:creator" content="' . str_replace('https://twitter.com/', '@', $this->manifest->metadata->author->socialLink) . '" />';
      }
      return $metadata;
    }
    /**
     * Generate or load the path to variations on the logo
     * @var string $height height of the icon as a string
     * @var string $width width of the icon as a string
     * @var string $format (optional) png or jpeg format to return image as
     * @return string path to the image (web visible) that was created or pulled together
     */
    public function getLogoSize($height, $width, $format = "png") {
      $fileName = &$this->staticCache(__FUNCTION__ . $height . $width);
      if (!isset($fileName)) {
        // if no logo, just bail with an easy standard one
        if (!isset($this->manifest->metadata->site->logo) || (isset($this->manifest->metadata->site) && ($this->manifest->metadata->site->logo == '' || $this->manifest->metadata->site->logo == null || $this->manifest->metadata->site->logo == "null"))) {
            $fileName = 'assets/icon-' . $height . 'x' . $width . '.png';
        }
        else {
          // ensure this path exists otherwise let's create it on the fly
          $path = '/';
          // support for default so we compress it using same engine
          if ($this->manifest->metadata->site->logo == 'assets/banner.jpg') {
            $fileName = str_replace('assets/', 'files/haxcms-managed/' . $height . 'x' . $width . '-', $this->manifest->metadata->site->logo);
          }
          else {
            $fileName = str_replace('files/', 'files/haxcms-managed/' . $height . 'x' . $width . '-', $this->manifest->metadata->site->logo);
          }
          // always replace this terrible name
          $fileName = str_replace('.jpeg', '.jpg', $fileName);
          if ($format == "jpg") {
            $fileName = str_replace('.png', '.jpg', $fileName);
          }
          else {
            $fileName = str_replace('.jpg', '.png', $fileName);
          }
          if (file_exists($path . $this->manifest->metadata->site->logo) && !file_exists($path . $fileName)) {
            global $fileSystem;
            $fileSystem->mkdir($path . 'files/haxcms-managed');
            $image = new ImageResize($path . $this->manifest->metadata->site->logo);
            if ($format == "png") {
              $image->resizeToBestFit($height, $width)
              ->crop($height, $width, TRUE)
              ->save($path . $fileName, IMAGETYPE_PNG, 9); // 9 is max compression on images
            }
            else if ($format == "jpg") {
              $image->resizeToBestFit($height, $width)
              ->crop($height, $width, TRUE)
              ->save($path . $fileName, IMAGETYPE_JPEG, 70); // jpeg compression
            }
          }
        }
      }
      return $fileName;
    }
    /**
     * License data for common open license
     */
    public function getLicenseData($type = 'select')
    {
        $list = array(
            "by" => array(
                'name' => "Creative Commons: Attribution",
                'link' => "https://creativecommons.org/licenses/by/4.0/",
                'image' => "https://i.creativecommons.org/l/by/4.0/88x31.png"
            ),
            "by-sa" => array(
                'name' => "Creative Commons: Attribution Share a like",
                'link' => "https://creativecommons.org/licenses/by-sa/4.0/",
                'image' => "https://i.creativecommons.org/l/by-sa/4.0/88x31.png"
            ),
            "by-nd" => array(
                'name' => "Creative Commons: Attribution No derivatives",
                'link' => "https://creativecommons.org/licenses/by-nd/4.0/",
                'image' => "https://i.creativecommons.org/l/by-nd/4.0/88x31.png"
            ),
            "by-nc" => array(
                'name' => "Creative Commons: Attribution non-commercial",
                'link' => "https://creativecommons.org/licenses/by-nc/4.0/",
                'image' => "https://i.creativecommons.org/l/by-nc/4.0/88x31.png"
            ),
            "by-nc-sa" => array(
                'name' =>
                    "Creative Commons: Attribution non-commercial share a like",
                'link' => "https://creativecommons.org/licenses/by-nc-sa/4.0/",
                'image' =>
                    "https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png"
            ),
            "by-nc-nd" => array(
                'name' =>
                    "Creative Commons: Attribution Non-commercial No derivatives",
                'link' => "https://creativecommons.org/licenses/by-nc-nd/4.0/",
                'image' =>
                    "https://i.creativecommons.org/l/by-nc-nd/4.0/88x31.png"
            )
        );
        $data = array();
        if ($type == 'select') {
            foreach ($list as $key => $item) {
                $data[$key] = $item['name'];
            }
        }
        else {
            $data = $list;
        }
        return $data;
    }
    /**
     * Request URI resolution
     */
    public function request_uri() {
      if (isset($_SERVER['REQUEST_URI'])) {
        $uri = $_SERVER['REQUEST_URI'];
      }
      else {
        if (isset($_SERVER['argv'])) {
          $uri = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['argv'][0];
        }
        elseif (isset($_SERVER['QUERY_STRING'])) {
          $uri = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['QUERY_STRING'];
        }
        else {
          $uri = $_SERVER['SCRIPT_NAME'];
        }
      }
      $uri = '/' . ltrim($uri, '/');
      return $uri;
    }
    /**
     * Load a node based on a path
     * @var $path the path to try loading based on or search for the active from address
     */
    public function loadNodeByLocation($path = NULL) {
      // load from the active address if we have one
      if (is_null($path)) {
        $opPath = str_replace($this->basePath . $this->sitesDirectory . '/' . $this->manifest->metadata->site->name . '/', '', $this->request_uri());
        $path = $opPath;
    }
      $path .= "/index.html";
      // failsafe in case someone had closing /
      $path = 'pages/' . str_replace('//', '/', $path);
      foreach ($this->manifest->items as $item) {
        if ($item->location == $path || $item->slug == $opPath) {
          return $item;
        }
      }
      return $this->manifest->items[0];
    }
    /**
     * Load wc-registry.json relative to the site in question
     */
    public function getWCRegistryJson($base = './') {
      $wcMap = &$this->staticCache(__FUNCTION__ . $this->manifest->metadata->site->name . $base);
      if (!isset($wcMap)) {
        // need to make the request relative to site
        if ($base == './') {
          // possible this comes up empty
          if (file_exists('./wc-registry.json')) {
            $wcPath = './wc-registry.json';
          }
        }
        else {
          $wcPath = $base . 'wc-registry.json';
        }
        // support private IP space which will block this ever going through
        if (!defined('IAM_PRIVATE_ADDRESS_SPACE')) {
          $wcMap = json_decode(file_get_contents($wcPath));
        }
      }
      return $wcMap;
    }
    /**
     * Load content of this page
     * @return string HTML / contents of the page object
     */
    public function getPageContent($page) {
      if (isset($page->location) && $page->location != '') {
        $content = &$this->staticCache(__FUNCTION__ . $page->location);
        if (!isset($content)) {
          $content = filter_var(file_get_contents('./' . $page->location));
        }
        return $content;
      }
    }
    /**
     * Get a social sharing image based on context of page or site having media
     * @var string $page page to mine the image from or attempt to
     * @return string full URL to an image
     */
    public function getSocialShareImage($page = null) {
      // resolve a JOS Item vs null
      if ($page != null) {
        $id = $page->id;
      }
      else {
        $id = null;
      }
      $fileName = &$this->staticCache(__FUNCTION__ . $id);

      if (!isset($fileName)) {
        if (is_null($page)) {
          $page = $this->loadNodeByLocation();
        }
        if (isset($page->metadata->files)) {
          foreach ($page->metadata->files as $file) {
            if ($file->type == 'image/jpeg') {
              $fileName = $file->fullUrl;
            }
          }
        }
        // look for the theme banner
        if (isset($this->manifest->metadata->theme->variables->image)) {
          $fileName = $this->manifest->metadata->theme->variables->image;
        }
      }
      return $fileName;
    }
    /**
     * Static cache a variable that may be called multiple times
     * in one transaction yet has same result
     */
    public function &staticCache($name, $default_value = NULL, $reset = FALSE) {
      static $data = array(), $default = array();
      if (isset($data[$name]) || array_key_exists($name, $data)) {
        if ($reset) {
          $data[$name] = $default[$name];
        }
        return $data[$name];
      }
      if (isset($name)) {
        if ($reset) {
          return $data;
        }
        $default[$name] = $data[$name] = $default_value;
        return $data[$name];
      }
      foreach ($default as $name => $value) {
        $data[$name] = $value;
      }
      return $data;
    }

    /**
     * Return the link to the cdn to use for serving dynamic pages
     * if $site defines a dynamicCDN endpoint then this overrides
     * any global setting. Useful for locally developed custom builds.
     */
    public function getCDNForDynamic() {
      if (isset($this->manifest->metadata->site->dynamicCDN)) {
        return $this->manifest->metadata->site->dynamicCDN;
      }
      return $this->cdn;
    }

    /**
     * Return the gaID which is the (optional) Google Analytics ID
     * @return string gaID the user put in
     */
    public function getGaID() {
      if (isset($this->manifest->metadata->site->settings->gaID) && $this->manifest->metadata->site->settings->gaID) {
          return $this->manifest->metadata->site->settings->gaID;
      }
      return null;
    }
    /**
     * Return the sw status
     * @return string status of forced upgrade, string as boolean since it'll get written into a JS file
     */
    public function getServiceWorkerStatus() {
        if (isset($this->manifest->metadata->site->settings->sw) && $this->manifest->metadata->site->settings->sw) {
            return TRUE;
        }
        return FALSE;
    }
        /**
     * Return a standard service worker that takes into account
     * the context of the page it's been placed on.
     * @todo this will need additional vetting based on the context applied
     * @return string <script> tag that will be a rather standard service worker
     */
    public function getServiceWorkerScript($basePath = null, $ignoreDevMode = FALSE, $addSW = TRUE) {
      // because this can screw with caching, let's make sure we
      // can throttle it locally for developers as needed
      if (!$addSW || ($this->developerMode && !$ignoreDevMode)) {
        return "\n  <!-- Service worker disabled via settings -->\n";
      }
      // support dynamic calculation
      if (is_null($basePath)) {
        $basePath = $this->basePath . $this->manifest->metadata->site->name . '/';
      }
      return "
  <script>
    if ('serviceWorker' in navigator) {
      var sitePath = '" . $basePath . "';
      // discover this path downstream of the root of the domain
      var swScope = window.location.pathname.substring(0, window.location.pathname.indexOf(sitePath)) + sitePath;
      if (swScope != document.head.getElementsByTagName('base')[0].href) {
        document.head.getElementsByTagName('base')[0].href = swScope;
      }
      window.addEventListener('load', function () {
        navigator.serviceWorker.register('service-worker.js', {
          scope: swScope
        }).then(function (registration) {
          registration.onupdatefound = function () {
            // The updatefound event implies that registration.installing is set; see
            // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-container-updatefound-event
            var installingWorker = registration.installing;
            installingWorker.onstatechange = function () {
              switch (installingWorker.state) {
                case 'installed':
                  if (!navigator.serviceWorker.controller) {
                    window.dispatchEvent(new CustomEvent('haxcms-toast-show', {
                      bubbles: true,
                      cancelable: false,
                      detail: {
                        text: 'Pages you view are cached for offline use.',
                        duration: 4000
                      }
                    }));
                  }
                break;
                case 'redundant':
                  throw Error('The installing service worker became redundant.');
                break;
              }
            };
          };
        }).catch(function (e) {
          console.warn('Service worker registration failed:', e);
        });
        // Check to see if the service worker controlling the page at initial load
        // has become redundant, since this implies there's a new service worker with fresh content.
        if (navigator.serviceWorker.controller) {
          navigator.serviceWorker.controller.onstatechange = function(event) {
            if (event.target.state === 'redundant') {
              var b = document.createElement('paper-button');
              b.appendChild(document.createTextNode('Reload'));
              b.raised = true;
              b.addEventListener('click', function(e){ window.location.reload(true); });
              window.dispatchEvent(new CustomEvent('haxcms-toast-show', {
                bubbles: true,
                cancelable: false,
                detail: {
                  text: 'A site update is available. Reload for latest content.',
                  duration: 8000,
                  slot: b,
                  clone: false
                }
              }));
            }
          };
        }
      });
    }
  </script>";
    }
  }
  $HAXSiteConfig = new HAXSiteConfig('./site.json');
}