/** * Parse a block of YAML into PHP * * @param string $yaml YAML-formatted string to parse * @param string $mode Parsing mode to use * @return array */ public static function parse($yaml, $mode = null) { // start measuring $hash = Debug::markStart('parsing', 'yaml'); $mode = $mode ? $mode : self::getMode(); switch ($mode) { case 'loose': $result = Spyc::YAMLLoad($yaml); break; case 'strict': $result = sYAML::parse($yaml); break; case 'transitional': try { $result = sYaml::parse($yaml); } catch (Exception $e) { Log::error($e->getMessage() . ' Falling back to loose mode.', 'core', 'yaml'); $result = Spyc::YAMLLoad($yaml); } break; default: // check for new lines, is this a file? $has_newline = strpos($yaml, "\n") !== false; if (!$has_newline && File::exists($yaml)) { // seems like it is $yaml = File::get($yaml); } $result = Statamic\Dipper\Dipper::parse($yaml); } // end measuring Debug::markEnd($hash); Debug::increment('parses', 'yaml'); return $result; }
/** * Parse a block of YAML into PHP * * @param string $yaml YAML-formatted string to parse * @param string $mode Parsing mode to use * @return array */ public static function parse($yaml, $mode = null) { // start measuring $hash = Debug::markStart('parsing', 'yaml'); $mode = $mode ? $mode : self::getMode(); switch ($mode) { case 'loose': $result = Spyc::YAMLLoad($yaml); break; case 'strict': $result = sYAML::parse($yaml); break; case 'transitional': try { $result = sYaml::parse($yaml); } catch (Exception $e) { Log::error($e->getMessage() . ' Falling back to loose mode.', 'core', 'yaml'); $result = Spyc::YAMLLoad($yaml); } break; default: $result = Spyc::YAMLLoad($yaml); } // end measuring Debug::markEnd($hash); Debug::increment('parses', 'yaml'); return $result; }
/** * Run the instance of a given hook * * @param string $namespace The namespace (addon/aspect) calling the hook * @param string $hook Name of hook * @param string $type Cumulative/replace/call * @param mixed $return Pass-through values * @param mixed $data Data to pass to hooked method * @return mixed */ public static function run($namespace, $hook, $type = NULL, $return = NULL, $data = NULL) { $mark_as_init = !self::$hooks_found; if (!self::$hooks_found) { $hash = Debug::markStart('hooks', 'finding'); // we went finding self::$hooks_found = true; // set paths $addons_path = BASE_PATH . Config::getAddOnsPath(); $bundles_path = APP_PATH . '/core/bundles'; $pattern = '/*/hooks.*.php'; // globbing with a brace doesn't seem to work on some system, // it's not just Windows-based servers, seems to affect some // linux-based ones too $bundles = glob($bundles_path . $pattern); $addons = glob($addons_path . $pattern); $bundles = empty($bundles) ? array() : $bundles; $addons = empty($addons) ? array() : $addons; self::$hook_files = array_merge($bundles, $addons); Debug::markEnd($hash); } $hash = Debug::markStart('hooks', 'running'); if (self::$hook_files) { foreach (self::$hook_files as $file) { $name = substr($file, strrpos($file, '/') + 7); $name = substr($name, 0, strlen($name) - 4); $class_name = 'Hooks_' . $name; if (!is_callable(array($class_name, $namespace . '__' . $hook), false)) { continue; } try { $hook_class = Resource::loadHooks($name); $method = $namespace . '__' . $hook; if ($type == 'cumulative') { $response = $hook_class->{$method}($data); if (is_array($response)) { $return = is_array($return) ? $return + $response : $response; } else { $return .= $response; } } elseif ($type == 'replace') { $return = $hook_class->{$method}($data); } else { $hook_class->{$method}($data); } } catch (Exception $e) { continue; } } } if ($mark_as_init) { Debug::markMilestone('hooks initialized'); } Debug::markEnd($hash); return $return; }
/** * Run the instance of a given hook * * @param string $namespace The namespace (addon/aspect) calling the hook * @param string $hook Name of hook * @param string $type Cumulative/replace/call * @param mixed $return Pass-through values * @param mixed $data Data to pass to hooked method * @return mixed */ public static function run($namespace, $hook, $type = NULL, $return = NULL, $data = NULL) { $mark_as_init = !self::$hooks_found; if (!self::$hooks_found) { $hash = Debug::markStart('hooks', 'finding'); // we went finding self::$hooks_found = true; // set paths $addons_path = BASE_PATH . Config::getAddOnsPath(); $bundles_path = APP_PATH . '/core/bundles'; $pattern = '/*/hooks.*.php'; // muuulllllti-gloooobbb self::$hook_files = glob('{' . $bundles_path . $pattern . ',' . $addons_path . $pattern . '}', GLOB_BRACE); Debug::markEnd($hash); } $hash = Debug::markStart('hooks', 'running'); if (self::$hook_files) { foreach (self::$hook_files as $file) { $name = substr($file, strrpos($file, '/') + 7); $name = substr($name, 0, strlen($name) - 4); $class_name = 'Hooks_' . $name; if (!is_callable(array($class_name, $namespace . '__' . $hook), false)) { continue; } try { $hook_class = Resource::loadHooks($name); $method = $namespace . '__' . $hook; if ($type == 'cumulative') { $response = $hook_class->{$method}($data); if (is_array($response)) { $return = is_array($return) ? $return + $response : $response; } else { $return .= $response; } } elseif ($type == 'replace') { $return = $hook_class->{$method}($data); } else { $hook_class->{$method}($data); } } catch (Exception $e) { continue; } } } if ($mark_as_init) { Debug::markMilestone('hooks initialized'); } Debug::markEnd($hash); return $return; }
public function partial() { $start = time(); $src = $this->fetchParam('src', null, null, false, false); $extensions = array(".html", ".md", ".markdown", ".textile"); $html = null; // measurement $hash = Debug::markStart('partials', $src, $start); Debug::increment('partials', $src); if ($src) { foreach ($extensions as $extension) { $full_src = Path::assemble(BASE_PATH, $this->theme_root, 'partials', ltrim($src . $extension, '/')); if (File::exists($full_src)) { Statamic_View::$_dataStore = $this->attributes + Statamic_View::$_dataStore; if ($this->fetchParam('use_context', false, false, true, false)) { // to use context, we only want to pass the attributes as // the current data, as those will override into the context // set of data; if we were to include all of ::$_dataStore, // we run into the issue where all of the global-level variables // are overriding variables in context, when the variables in // context are more accurate scope-wise at this point in the parse $html = Parse::contextualTemplate(file_get_contents($full_src), $this->attributes, $this->context, array('statamic_view', 'callback'), true); } else { $html = Parse::template(file_get_contents($full_src), Statamic_View::$_dataStore); } // parse contents if needed if ($extension == ".md" || $extension == ".markdown") { $html = Parse::markdown($html); } elseif ($extension == ".textile") { $html = Parse::textile($html); } } } if (Config::get('enable_smartypants', TRUE)) { $html = Parse::smartypants($html); } } Debug::markEnd($hash); return $html; }
/** * Supplements the content in the set * * @param array $context Context for supplementing * @return void */ public function supplement($context = array()) { $hash = Debug::markStart('content', 'supplementing'); if ($this->supplemented) { return; } $this->supplemented = true; $context = Helper::ensureArray($context); // determine context $given_context = $context; $context = array('locate_with' => isset($given_context['locate_with']) ? $given_context['locate_with'] : null, 'center_point' => isset($given_context['center_point']) ? $given_context['center_point'] : null, 'list_helpers' => isset($given_content['list_helpers']) ? $given_context['list_helpers'] : true, 'context_urls' => isset($given_context['context_urls']) ? $given_context['context_urls'] : true, 'total_found' => isset($given_context['total_found']) ? $given_context['total_found'] : null, 'group_by_date' => isset($given_context['group_by_date']) ? $given_context['group_by_date'] : null, 'inherit_folder_data' => isset($given_context['inherit_folder_data']) ? $given_context['inherit_folder_data'] : true, 'merge_with_data' => isset($given_context['merge_with_data']) ? $given_context['merge_with_data'] : true); // set up helper variables $center_point = false; if ($context['center_point'] && preg_match(Pattern::COORDINATES, $context['center_point'], $matches)) { $center_point = array($matches[1], $matches[2]); } // contextual urls are based on current page, not individual data records // we can figure this out once and then set it with each one if ($context['context_urls']) { $raw_url = Request::getResourceURI(); $page_url = Path::tidy($raw_url); } // iteration memory $last_date = null; // loop through content, supplementing each record with data foreach ($this->content as $content_key => $data) { // locate if ($context['locate_with']) { $location_data = isset($data[$context['locate_with']]) ? $data[$context['locate_with']] : null; // check that location data is fully set if (is_array($location_data) && isset($location_data['latitude']) && $location_data['latitude'] && isset($location_data['longitude']) && $location_data['longitude']) { $data['latitude'] = $location_data['latitude']; $data['longitude'] = $location_data['longitude']; $data['coordinates'] = $location_data['latitude'] . "," . $location_data['longitude']; // get distance from center if ($center_point) { $location = array($data['latitude'], $data['longitude']); $data['distance_km'] = Math::getDistanceInKilometers($center_point, $location); $data['distance_mi'] = Math::convertKilometersToMiles($data['distance_km']); } } } // contextual urls if ($context['context_urls']) { $data['raw_url'] = $raw_url; $data['page_url'] = $page_url; } // total entries if ($context['total_found']) { $data['total_found'] = (int) $context['total_found']; } // group by date if ($context['group_by_date'] && $data['datestamp']) { $formatted_date = Date::format($context['group_by_date'], $data['datestamp']); if ($formatted_date !== $last_date) { $last_date = $formatted_date; $data['grouped_date'] = $formatted_date; } else { $data['grouped_date'] = ''; } } // loop through content to add data for variables that are arrays foreach ($data as $key => $value) { // Only run on zero indexed arrays/loops if (is_array($value) && isset($value[0]) && !is_array($value[0])) { // list helpers if ($context['list_helpers']) { // make automagic lists $data[$key . "_list"] = join(", ", $value); $data[$key . "_spaced_list"] = join(" ", $value); $data[$key . "_option_list"] = join("|", $value); $data[$key . "_ordered_list"] = "<ol><li>" . join("</li><li>", $value) . "</li></ol>"; $data[$key . "_unordered_list"] = "<ul><li>" . join("</li><li>", $value) . "</li></ul>"; $data[$key . "_sentence_list"] = Helper::makeSentenceList($value); $data[$key . "_ampersand_sentence_list"] = Helper::makeSentenceList($value, "&", false); // handle taxonomies if (Taxonomy::isTaxonomy($key)) { $url_list = array_map(function ($item) use($data, $key, $value) { return '<a href="' . Taxonomy::getURL($data['_folder'], $key, $item) . '">' . $item . '</a>'; }, $value); $data[$key . "_url_list"] = join(", ", $url_list); $data[$key . "_spaced_url_list"] = join(" ", $url_list); $data[$key . "_ordered_url_list"] = "<ol><li>" . join("</li><li>", $url_list) . "</li></ol>"; $data[$key . "_unordered_url_list"] = "<ul><li>" . join("</li><li>", $url_list) . "</li></ul>"; $data[$key . "_sentence_url_list"] = Helper::makeSentenceList($url_list); $data[$key . "_ampersand_sentence_url_list"] = Helper::makeSentenceList($url_list, "&", false); } } } } // update content with supplemented data merged with global config data if ($context['merge_with_data'] || $context['inherit_folder_data']) { $folder_data = array(); $all_config = array(); if ($context['inherit_folder_data']) { $folder_data = $this->getFolderData($data['_file']); } if ($context['merge_with_data']) { $all_config = Config::getAll(); } // merge them all together $this->content[$content_key] = $data + $folder_data + $all_config; } else { $this->content[$content_key] = $data; } } Debug::markEnd($hash); }
/** * Parses a conditions string * * @param string $conditions Conditions to parse * @return array */ public static function conditions($conditions) { // start measuring $hash = Debug::markStart('parsing', 'conditions'); Debug::increment('parses', 'condition_statements'); $replacement = '__TEMP_COMMA_' . substr(md5(time()), 0, 12) . '__'; $conditions = explode(",", str_replace('\\,', $replacement, $conditions)); $output = array(); foreach ($conditions as $condition) { Debug::increment('parses', 'conditions'); $result = Parse::condition(str_replace($replacement, ',', $condition)); $output[$result['key']] = $result['value']; } // end measuring Debug::markEnd($hash); return $output; }
/** * Takes a scope-notated key and finds the value for it in the given * array or object. * * @param string $key Dot-notated key to find * @param array|object $data Array or object to search * @param mixed $default Default value to use if not found * @return mixed */ protected function getVariable($key, $data, $default = null) { // <statamic> // detect modifiers $modifiers = null; if (strpos($key, "|") !== false) { $parts = explode("|", $key); $key = $parts[0]; $modifiers = array_splice($parts, 1); } // </statamic> if (strpos($key, $this->scopeGlue) === false) { $parts = explode('.', $key); } else { $parts = explode($this->scopeGlue, $key); } foreach ($parts as $key_part) { if (is_array($data)) { if (!array_key_exists($key_part, $data)) { return $default; } $data = $data[$key_part]; } elseif (is_object($data)) { if (!isset($data->{$key_part})) { return $default; } $data = $data->{$key_part}; } else { return $default; } } // <statamic> // execute modifier chain if ($modifiers) { foreach ($modifiers as $mod) { $now = time(); if (strpos($mod, ":") === false) { $modifier = $mod; $modifier_params = array(); } else { $parts = explode(":", $mod); $modifier = $parts[0]; $modifier_params = array_splice($parts, 1); } $hash = \Debug::markStart('modifiers', $modifier, $now); try { // load modifier $modifier_obj = \Resource::loadModifier(\Parse::modifierAlias($modifier)); // ensure method exists if (!method_exists($modifier_obj, "index")) { throw new \Exception("Improperly formatted modifier object."); } // call method $data = $modifier_obj->index($data, $modifier_params); \Debug::increment('modifiers', $modifier); } catch (\Exception $e) { // do nothing } \Debug::markEnd($hash); } } // </statamic> return $data; }
/** * Load the config (yaml) files in a specified order: * * 1. Loose per-site configs * 2. Routes * 3. Settings * 4. Theme overrides */ public static function loadAllConfigs($admin = false) { $hash = Debug::markStart('config', 'finding'); /* |-------------------------------------------------------------------------- | YAML Mode |-------------------------------------------------------------------------- | | We need to know the YAML mode first (loose, strict, transitional), | so we parse the settings file once to check before doing anything else. | */ $preload_config = YAML::parse(Config::getConfigPath() . '/settings.yaml'); $yaml_mode = array_get($preload_config, '_yaml_mode', 'loose'); /* |-------------------------------------------------------------------------- | Default Settings |-------------------------------------------------------------------------- | | We keep a set of default options that the user config overrides, allowing | us to always have clean defaults. | */ $settings_to_parse = File::get(Config::getAppConfigPath() . '/default.settings.yaml'); /* |-------------------------------------------------------------------------- | User Site Settings |-------------------------------------------------------------------------- | | Next we parse and override the user's settings. | */ $settings_to_parse .= "\n\n" . File::get(Config::getConfigPath() . '/settings.yaml'); /* |-------------------------------------------------------------------------- | Routes and vanity URLs |-------------------------------------------------------------------------- | | Any URL can be manipulated by routes or vanity urls. We need this info | early on, before content parsing begins. | */ $settings_to_parse .= "\n\n_routes:\n " . trim(preg_replace("/\n/", "\n ", File::get(Config::getConfigPath() . '/routes.yaml'))); $settings_to_parse .= "\n\n_vanity_urls:\n " . trim(preg_replace("/\n/", "\n ", File::get(Config::getConfigPath() . '/vanity.yaml'))); /* |-------------------------------------------------------------------------- | Global Variables |-------------------------------------------------------------------------- | | We parse all the yaml files in the root (except settings and routes) of | the config folder and make them available as global template variables. | */ if (Folder::exists($config_files_location = Config::getConfigPath())) { $files = glob($config_files_location . '/*.yaml'); if ($files) { foreach ($files as $file) { if (strpos($file, 'routes.yaml') !== false || strpos($file, 'vanity.yaml') !== false || strpos($file, 'settings.yaml')) { continue; } $settings_to_parse .= "\n\n" . File::get($file); } } } /* |-------------------------------------------------------------------------- | Parse settings up until now |-------------------------------------------------------------------------- | | Parses the concatenated settings string we've made so far. | */ $config = YAML::parse($settings_to_parse, $yaml_mode); /* |-------------------------------------------------------------------------- | Theme Variables |-------------------------------------------------------------------------- | | Theme variables need to specifically parsed later so they can override | any site/global defaults. | */ $themes_path = array_get($config, '_themes_path', '_themes'); $theme_name = array_get($config, '_theme', 'acadia'); // reset $settings_to_parse = ''; if (Folder::exists($theme_files_location = Path::assemble(BASE_PATH, $themes_path, $theme_name))) { $theme_files = glob(Path::tidy($theme_files_location . '/*.yaml')); if ($theme_files) { foreach ($theme_files as $file) { $settings_to_parse .= "\n\n" . File::get($file); } } } Debug::markEnd($hash); // parse theme settings if any if ($settings_to_parse) { $config = YAML::parse($settings_to_parse, $yaml_mode) + $config; } /* |-------------------------------------------------------------------------- | Load Environment Configs and Variables |-------------------------------------------------------------------------- | | Environments settings explicitly overwrite any existing settings, and | therefore must be loaded late. We also set a few helper variables | to make working with environments even easier. | */ _Environment::establish($config); /* |-------------------------------------------------------------------------- | MIME Types |-------------------------------------------------------------------------- */ $config = array('_mimes' => require Config::getAppConfigPath() . '/mimes.php') + $config; /* |-------------------------------------------------------------------------- | Localization |-------------------------------------------------------------------------- | | We load up English by default. We're American after all. Doesn't the | world revolve around us? Hello? Bueller? More hamburgers please. | */ $config['_translations'] = array(); $config['_translations']['en'] = YAML::parse(Config::getAppConfigPath() . '/default.en.yaml'); if ($lang = array_get($config, '_language', false)) { if (File::exists(Config::getTranslation($lang))) { $translation = YAML::parse(Config::getTranslation($lang)); $config['_translations'][$lang] = Helper::arrayCombineRecursive($config['_translations']['en'], $translation); } } $finder = new Finder(); // clear previous Finder interator results try { $translation_files = $finder->files()->in(BASE_PATH . Config::getAddonsPath() . '/*/translations')->name($lang . '.*.yaml')->depth(0)->followLinks(); foreach ($translation_files as $file) { $translation = YAML::parse($file->getRealPath()); $config['_translations'][$lang] = Helper::arrayCombineRecursive($translation, $config['_translations'][$lang]); } } catch (Exception $e) { // meh. not important. } /* |-------------------------------------------------------------------------- | Set Slim Config |-------------------------------------------------------------------------- | | Slim needs to be initialized with a set of config options, so these | need to be set earlier than the set_default_tags() method. | */ // $config['view'] = new Statamic_View(); $config['cookies.lifetime'] = $config['_cookies.lifetime']; if ($admin) { $admin_theme = array_get($config, '_admin_theme', 'ascent'); if (!Folder::exists(BASE_PATH . Path::tidy('/' . $config['_admin_path'] . '/' . 'themes/' . $admin_theme))) { $admin_theme = 'ascent'; } $theme_path = Path::tidy('/' . $config['_admin_path'] . '/' . 'themes/' . $admin_theme . '/'); $config['theme_path'] = $theme_path; $config['templates.path'] = '.' . $theme_path; } else { $public_path = isset($config['_public_path']) ? $config['_public_path'] : ''; $config['theme_path'] = $themes_path . '/' . $config['_theme'] . '/'; $config['templates.path'] = Path::tidy($public_path . $themes_path . '/' . $config['_theme'] . '/'); } if (!array_get($config, '_display_debug_panel', false)) { Debug::disable(); } return $config; }
/** * Finds content by path * * @param string $path Path to use to look for content * @return array|false */ public static function find($path) { $hash = Debug::markStart('content', 'finding'); // ensure it starts with / $path = Path::tidy('/' . $path); ContentService::loadCache(); $urls = ContentService::$cache['urls']; foreach ($urls as $url => $data) { if ($data['path'] === $path) { return Content::get($url, false, false); } } Debug::markEnd($hash); return false; }
/** * callback * Attempts to load a plugin? * * @param string $name * @param array $attributes * @param string $content * @param array $context * @return string * @throws Exception */ public static function callback($name, $attributes, $content, $context = array()) { $now = time(); $output = false; $pos = strpos($name, ':'); # single function plugins if ($pos === false) { $plugin = $name; $call = "index"; } else { $plugin = substr($name, 0, $pos); $call = substr($name, $pos + 1); } // mark start of debug timing $hash = Debug::markStart('plugins', $plugin . ':' . $call, $now); // if nothing to call, abort if (!$call) { Debug::markEnd($hash); return null; } try { // will throw an exception if resource isn't available $plugin_obj = Resource::loadPlugin($plugin); if (!is_callable(array($plugin_obj, $call))) { throw new Exception('Method not callable.'); } $plugin_obj->attributes = $attributes; $plugin_obj->content = $content; $plugin_obj->context = $context; Debug::increment('plugins', $plugin); $output = call_user_func(array($plugin_obj, $call)); if (is_array($output)) { $output = Parse::template($content, $output); } } catch (\Slim\Exception\Stop $e) { // allow plugins to halt the app throw $e; } catch (ResourceNotFoundException $e) { // resource not found, do nothing } catch (Exception $e) { // everything else, do nothing if debug is off if (Config::get('debug')) { throw $e; } } Debug::markEnd($hash); return $output; }
/** * Fetch a single content entry or page * * @param string $url URL to fetch * @param bool $parse_content Should we parse content? * @param bool $supplement Should we supplement the content? * @return array */ public static function get($url, $parse_content = true, $supplement = true) { $hash = Debug::markStart('content', 'getting'); $url_hash = Helper::makeHash($url, $parse_content, $supplement); if (!isset(self::$fetched_content[$url_hash])) { $content_set = ContentService::getContentByURL($url); $content = $content_set->get($parse_content, $supplement); self::$fetched_content[$url_hash] = isset($content[0]) ? $content[0] : array(); } Debug::markEnd($hash); return self::$fetched_content[$url_hash]; }
/** * Creates a hash value for the arguments passed * * @param mixed ... Arguments to include in hash * @return string */ public static function makeHash() { $hash = Debug::markStart('math', 'hashing'); $args = func_get_args(); $data = array(); // loop through arguments, adding flattened versions to $data foreach ($args as $arg) { if (is_array($arg)) { array_push($data, join('|', $arg)); } elseif (is_bool($arg)) { array_push($data, ($arg) ? 'true' : 'false'); } else { array_push($data, $arg); } } // return a hash of the flattened $data array $result = md5(join('%', $data)); Debug::markEnd($hash); return $result; }
/** * Creates a hash value for the arguments passed * * @param mixed ... Arguments to include in hash * @return string */ public static function makeHash() { $mark = Debug::markStart('math', 'hashing'); $data = array_flatten(func_get_args()); // return a hash of the flattened $data array $hash = md5(join('%', $data)); Debug::markEnd($mark); return $hash; }
/** * Updates the internal content cache * * @return boolean */ public static function update() { // start measuring $content_hash = Debug::markStart('caching', 'content'); // track if any files have changed $files_changed = false; $settings_changed = false; $members_changed = false; // grab length of content type extension $content_type = Config::getContentType(); $full_content_root = rtrim(Path::tidy(BASE_PATH . "/" . Config::getContentRoot()), "/"); $content_type_length = strlen($content_type) + 1; // the cache files we'll use $cache_file = BASE_PATH . '/_cache/_app/content/content.php'; $settings_file = BASE_PATH . '/_cache/_app/content/settings.php'; $structure_file = BASE_PATH . '/_cache/_app/content/structure.php'; $time_file = BASE_PATH . '/_cache/_app/content/last.php'; $members_file = BASE_PATH . '/_cache/_app/members/members.php'; $now = time(); // start measuring settings hash $settings_hash = Debug::markStart('caching', 'settings'); // check for current and new settings $settings = unserialize(File::get($settings_file)); if (!is_array($settings)) { $settings = array('site_root' => '', 'site_url' => '', 'timezone' => '', 'date_format' => '', 'time_format' => '', 'content_type' => '', 'taxonomy' => '', 'taxonomy_case_sensitive' => '', 'taxonomy_force_lowercase' => '', 'entry_timestamps' => '', 'base_path' => '', 'app_version' => ''); } // look up current settings $current_settings = array('site_root' => Config::getSiteRoot(), 'site_url' => Config::getSiteURL(), 'timezone' => Config::get('timezone'), 'date_format' => Config::get('date_format'), 'time_format' => Config::get('time_format'), 'content_type' => Config::get('content_type'), 'taxonomy' => Config::getTaxonomies(), 'taxonomy_case_sensitive' => Config::getTaxonomyCaseSensitive(), 'taxonomy_force_lowercase' => Config::getTaxonomyForceLowercase(), 'entry_timestamps' => Config::getEntryTimestamps(), 'base_path' => BASE_PATH, 'app_version' => STATAMIC_VERSION); // have cache-altering settings changed? if ($settings !== $current_settings) { // settings have changed $settings_changed = true; // clear the cache and set current settings $cache = self::getCleanCacheArray(); $settings = $current_settings; $last = null; } else { // grab the existing cache $cache = unserialize(File::get($cache_file)); if (!is_array($cache)) { $cache = self::getCleanCacheArray(); } $last = File::get($time_file); } // mark end of settings hash measuring Debug::markEnd($settings_hash); // grab a list of all content files $files = File::globRecursively(Path::tidy(BASE_PATH . '/' . Config::getContentRoot() . '/*'), Config::getContentType()); // grab a separate list of files that have changed since last check $updated = array(); $current_files = array(); // loop through files, getting local paths and checking for updated files foreach ($files as $file) { $local_file = Path::trimFilesystemFromContent(Path::standardize($file)); // add to current files $current_files[] = $local_file; // is this updated? if ($last && File::getLastModified($file) >= $last) { $updated[] = $local_file; } } // get a diff of files we know about and files currently existing $known_files = array(); foreach ($cache['urls'] as $url_data) { array_push($known_files, $url_data['path']); } $new_files = array_diff($current_files, $known_files); // create a master list of files that need updating $changed_files = array_unique(array_merge($new_files, $updated)); // store a list of changed URLs $changed_urls = array(); // add to the cache if files have been updated if (count($changed_files)) { $files_changed = true; // build content cache foreach ($changed_files as $file) { $file = $full_content_root . $file; $local_path = Path::trimFilesystemFromContent($file); // before cleaning anything, check for hidden or draft content $is_hidden = Path::isHidden($local_path); $is_draft = Path::isDraft($local_path); // now clean up the path $local_filename = Path::clean($local_path); // file parsing $content = substr(File::get($file), 3); $divide = strpos($content, "\n---"); $front_matter = trim(substr($content, 0, $divide)); $content_raw = trim(substr($content, $divide + 4)); // parse data $data = YAML::parse($front_matter); if ($content_raw) { $data['content'] = 'true'; $data['content_raw'] = 'true'; } // set additional information $data['_file'] = $file; $data['_local_path'] = $local_path; $data['_order_key'] = null; $data['datetimestamp'] = null; // legacy $data['datestamp'] = null; $data['date'] = null; $data['time'] = null; $data['numeric'] = null; $data['last_modified'] = filemtime($file); $data['_is_hidden'] = $is_hidden; $data['_is_draft'] = $is_draft; // get initial slug (may be changed below) $data['slug'] = ltrim(basename($file, "." . $content_type), "_"); // folder $instance = $data['slug'] == 'page' ? 1 : 0; $data['_folder'] = Path::clean($data['_local_path']); $slash = Helper::strrpos_count($data['_folder'], '/', $instance); $data['_folder'] = !$slash ? '' : substr($data['_folder'], 1, $slash - 1); $data['_folder'] = !strlen($data['_folder']) ? "/" : $data['_folder']; $data['_basename'] = $data['slug'] . '.' . $content_type; $data['_filename'] = $data['slug']; $data['_is_entry'] = preg_match(Pattern::ENTRY_FILEPATH, $data['_basename']); $data['_is_page'] = preg_match(Pattern::PAGE_FILEPATH, $data['_basename']); // 404 is special if ($data['_local_path'] === "/404.{$content_type}") { $local_filename = $local_path; // order key: date or datetime } elseif (preg_match(Pattern::DATE_OR_DATETIME, $data['_basename'], $matches)) { // order key: date or datetime $date = $matches[1] . '-' . $matches[2] . '-' . $matches[3]; $time = null; if (Config::getEntryTimestamps() && isset($matches[4])) { $time = substr($matches[4], 0, 2) . ":" . substr($matches[4], 2); $date = $date . " " . $time; $data['slug'] = substr($data['slug'], 16); $data['datetimestamp'] = $data['_order_key']; } else { $data['slug'] = substr($data['slug'], 11); } $data['_order_key'] = strtotime($date); $data['datestamp'] = $data['_order_key']; $data['date'] = Date::format(Config::getDateFormat(), $data['_order_key']); $data['time'] = $time ? Date::format(Config::getTimeFormat(), $data['_order_key']) : null; // order key: slug is page, back up a level } elseif ($data['slug'] == 'page' && preg_match(Pattern::NUMERIC, substr($data['_local_path'], Helper::strrpos_count($data['_local_path'], '/', 1)), $matches)) { // order key: slug is page, back up a level $data['_order_key'] = $matches[1]; $data['numeric'] = $data['_order_key']; // order key: numeric } elseif (preg_match(Pattern::NUMERIC, $data['_basename'], $matches)) { // order key: numeric $data['_order_key'] = $matches[1]; $data['numeric'] = $data['_order_key']; $data['slug'] = substr($data['slug'], strlen($matches[1]) + 1); // order key: other } else { // order key: other $data['_order_key'] = $data['_basename']; } // determine url $data['url'] = preg_replace('#/__?#', '/', $local_filename); // remove any content type extensions from the end of filename if (substr($data['url'], -$content_type_length) === '.' . $content_type) { $data['url'] = substr($data['url'], 0, strlen($data['url']) - $content_type_length); } // remove any base pages from filename if (substr($data['url'], -5) == '/page') { $data['url'] = substr($data['url'], 0, strlen($data['url']) - 5); } // add the site root $data['url'] = Path::tidy(Config::getSiteRoot() . $data['url']); // add the site URL to get the permalink $data['permalink'] = Path::tidy(Config::getSiteURL() . $data['url']); // new content if (!isset($cache['content'][$data['_folder']]) || !is_array($cache['content'][$data['_folder']])) { $cache['content'][$data['_folder']] = array(); } $slug_with_extension = $data['_filename'] == 'page' ? substr($data['url'], strrpos($data['url'], '/') + 1) . '/' . $data['_filename'] . "." . $content_type : $data['_filename'] . "." . $content_type; $cache['content'][$data['_folder']][$slug_with_extension] = array('folder' => $data['_folder'], 'path' => $local_path, 'file' => $slug_with_extension, 'url' => $data['url'], 'data' => $data); $cache['urls'][$data['url']] = array('folder' => $data['_folder'], 'path' => $local_path, 'file' => $slug_with_extension); $changed_urls[$data['url']] = true; } } // loop through all cached content for deleted files // this isn't as expensive as you'd think in real-world situations foreach ($cache['content'] as $folder => $folder_contents) { foreach ($folder_contents as $path => $data) { if (File::exists($full_content_root . $data['path'])) { // still here, keep it continue; } $files_changed = true; // get URL $url = isset($cache['content'][$folder][$path]['url']) ? $cache['content'][$folder][$path]['url'] : null; // only remove from URLs list if not in changed URLs list if (!isset($changed_urls[$url]) && !is_null($url)) { // remove from url cache unset($cache['urls'][$url]); } // remove from content cache unset($cache['content'][$folder][$path]); } } // build taxonomy cache // only happens if files were added, updated, or deleted above if ($files_changed) { $taxonomies = Config::getTaxonomies(); $force_lowercase = Config::getTaxonomyForceLowercase(); $case_sensitive = Config::getTaxonomyCaseSensitive(); $cache['taxonomies'] = array(); // rebuild taxonomies if (count($taxonomies)) { // set up taxonomy array foreach ($taxonomies as $taxonomy) { $cache['taxonomies'][$taxonomy] = array(); } // loop through content to build cached array foreach ($cache['content'] as $pages) { foreach ($pages as $item) { $data = $item['data']; // loop through the types of taxonomies foreach ($taxonomies as $taxonomy) { // if this file contains this type of taxonomy if (isset($data[$taxonomy])) { $values = Helper::ensureArray($data[$taxonomy]); // add the file name to the list of found files for a given taxonomy value foreach ($values as $value) { if (!$value) { continue; } $key = !$case_sensitive ? strtolower($value) : $value; if (!isset($cache['taxonomies'][$taxonomy][$key])) { $cache['taxonomies'][$taxonomy][$key] = array('name' => $force_lowercase ? strtolower($value) : $value, 'files' => array()); } array_push($cache['taxonomies'][$taxonomy][$key]['files'], $data['url']); } } } } } } // build structure cache $structure = array(); $home = Path::tidy('/' . Config::getSiteRoot() . '/'); foreach ($cache['content'] as $pages) { foreach ($pages as $item) { // set up base variables $parent = null; // Trim off home and any /page.md ending so that all URLs are treated // equally regardless of page type. $order_key = str_replace('/page.md', '', str_replace($home, '', $item['path'])); $sub_order_key = $item['data']['_order_key']; // does this have a parent (and if so, what is it?) if ($item['url'] !== $home) { $parent = $home; $depth = substr_count(str_replace($home, '/', $item['url']), '/'); $last_slash = strrpos($item['url'], '/', 1); $last_order_slash = strrpos($order_key, '/', 0); if ($last_slash !== false) { $parent = substr($item['url'], 0, $last_slash); } if ($last_order_slash !== false) { $order_key = substr($order_key, 0, $last_order_slash); } if ($item['data']['_is_page']) { $type = $item['data']['slug'] == 'page' ? 'folder' : 'page'; } else { $type = 'entry'; } } else { $depth = 0; $type = 'folder'; $order_key = $home; } $structure[$item['url']] = array('parent' => $parent, 'is_entry' => $item['data']['_is_entry'], 'is_page' => $item['data']['_is_page'], 'is_hidden' => $item['data']['_is_hidden'], 'is_draft' => $item['data']['_is_draft'], 'depth' => $depth, 'order_key' => $order_key ? $order_key : $sub_order_key, 'sub_order_key' => $sub_order_key, 'type' => $type); } } } // mark ending of content cache measuring Debug::markEnd($content_hash); if (!Config::get('disable_member_cache')) { // build member cache // ---------------------------------------------------------------- // start measuring $member_hash = Debug::markStart('caching', 'member'); // grab a list of existing members $users = File::globRecursively(Path::tidy(Config::getConfigPath() . '/users/*'), 'yaml'); // clone for reuse, set up our list of updated users $updated = array(); $current_users = array(); foreach ($users as $user) { $local_file = Path::trimFilesystemFromContent(Path::standardize($user)); // add to current users $current_users[] = $local_file; // is this updated? if ($last && File::getLastModified($user) >= $last) { $updated[] = $local_file; } } // get users from the file $members = unserialize(File::get($members_file)); // get a diff of users we know about and files currently existing $known_users = array(); if (!empty($members)) { foreach ($members as $username => $member_data) { $known_users[$username] = $member_data['_path']; } } // create a master list of users that need updating $changed_users = array_unique(array_merge(array_diff($current_users, $known_users), $updated)); $removed_users = array_diff($known_users, $current_users); if (count($changed_users)) { $members_changed = true; foreach ($changed_users as $user_file) { // file parsing $last_slash = strrpos($user_file, '/') + 1; $last_dot = strrpos($user_file, '.'); $username = substr($user_file, $last_slash, $last_dot - $last_slash); $content = substr(File::get($user_file), 3); $divide = strpos($content, "\n---"); $data = YAML::parse(trim(substr($content, 0, $divide))); $bio_raw = trim(substr($content, $divide + 4)); $data['_path'] = $user_file; if ($bio_raw) { $data['biography'] = 'true'; $data['biography_raw'] = 'true'; } $members[$username] = $data; } } // loop through all cached content for deleted files // this isn't as expensive as you'd think in real-world situations if (!empty($removed_users)) { $members_changed = true; $members = array_diff_key($members, $removed_users); } // mark ending of member cache measuring Debug::markEnd($member_hash); } // write to caches // -------------------------------------------------------------------- // add file-writing to content-cache actions $content_hash = Debug::markStart('caching', 'content'); if ($files_changed) { // store the content cache if (File::put($cache_file, serialize($cache)) === false) { if (!File::isWritable($cache_file)) { Log::fatal('Cache folder is not writable.', 'core', 'content-cache'); } Log::fatal('Could not write to the cache.', 'core', 'content-cache'); return false; } // store the structure cache if (File::put($structure_file, serialize($structure)) === false) { if (!File::isWritable($structure_file)) { Log::fatal('Structure cache file is not writable.', 'core', 'structure-cache'); } Log::fatal('Could not write to the structure cache.', 'core', 'structure-cache'); return false; } } // mark ending of content cache file write measuring Debug::markEnd($content_hash); // add file-writing to settings-cache actions $settings_hash = Debug::markStart('caching', 'settings'); // store the settings cache if ($settings_changed) { if (File::put($settings_file, serialize($settings)) === false) { if (!File::isWritable($settings_file)) { Log::fatal('Settings cache file is not writable.', 'core', 'settings-cache'); } Log::fatal('Could not write to the settings cache file.', 'core', 'settings-cache'); return false; } } // mark ending of settings cache file write measuring Debug::markEnd($settings_hash); if (!Config::get('disable_member_cache')) { // add file-writing to settings-cache actions $member_hash = Debug::markStart('caching', 'member'); // store the members cache if ($members_changed) { if (File::put($members_file, serialize($members)) === false) { if (!File::isWritable($members_file)) { Log::fatal('Member cache file is not writable.', 'core', 'member-cache'); } Log::fatal('Could not write to the member cache file.', 'core', 'member-cache'); return false; } } // mark ending of member cache file write measuring Debug::markEnd($member_hash); } File::put($time_file, $now - 1); return true; }