grav/plugins/add-page-by-form/add-page-by-form.php

970 lines
41 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Grav\Plugin;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Form\FormFlash;
use Grav\Common\Grav;
use Grav\Common\Page\Page;
use Grav\Common\Plugin;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\File;
use RocketTheme\Toolbox\File\YamlFile;
use Symfony\Component\Yaml\Yaml;
/**
* Class AddPageByFormPlugin
* @package Grav\Plugin
*/
class AddPageByFormPlugin extends Plugin
{
private $new_page_route = '';
private $move_self_files = false;
private $say_my_name = ['addpage', 'add_page'];
private $uploads = array();
private $page_frontmatter = array();
protected $post = array();
/**
* Extends a path
*
* @param string $path
* @param string $page
* @param string $slug
*
* @return string $path
*/
public function buildPath($path, $page, $slug)
{
if (!is_null($page)) {
return $path . DS . $page->folder();
} else {
return $path . DS . $slug;
}
}
/**
* Extends a route
*
* @param string $route
* @param string $page
* @param string $slug
*
* @return string $route
*/
public function buildRoute($route, $page, $slug)
{
if (!is_null($page)) {
return $route . DS . $page->slug();
} else {
return $route . DS . $slug;
}
}
/**
* Copy uploaded files and return list of properties
*
* @param string $form
* @param string $new_page
*
* @return array $file_fields
*/
public function copyFiles($form, $new_page)
{
// Copy uploaded files to the new page folder
// and prepare uploaded file properties
$new_page_path = $new_page->relativePagePath();
$file_fields = array();
$deleted_files = array();
$flash = $form->getFlash();
$fields = $flash->getFilesByFields();
foreach ($fields as $file_field => $uploads) {
$file_fields[$file_field] = array();
foreach ($uploads as $file_name => $upload) {
// When upload field has been emptied $upload == null
if ($upload) {
$file_name = $upload->getClientFilename();
$destination = $upload->getDestination();
// Do the actual file copy unless destination is in tmp/forms
if (strpos($destination, 'tmp/forms') === false) {
$full_name = $new_page_path . DS . $file_name;
$upload->moveTo($full_name);
} else {
$full_name = $destination;
// Leave the copy to the Form plugin
}
$file_fields[$file_field][$file_name] = [
'name' => $file_name,
'type' => $upload->getClientMediaType(),
'size' => $upload->getSize(),
'path' => $full_name
];
}
else {
// Uploaded file was removed by the user
$to_be_deleted = $new_page_path . DS . $file_name;
array_push($deleted_files, array('file_field' => $file_field, 'file_path' => $to_be_deleted));
}
}
}
return array('uploaded' => $file_fields, 'deleted' => $deleted_files);
}
/**
*
* @param string $route
* @param boolean $create
*
* @return array|null [route, path]
*
* Crawl a route, using modular page folder names as a fallback and
* optionally creating non existing folders along the way
*/
public function crawlRoute($route, $create = false)
{
$path = USER_DIR . 'pages';
if (!is_null($route) && isset($route)) {
if ($route != DS) {
$slugs = explode(DS, $route);
$route = '';
for ($i = 1; $i < count($slugs); $i++) {
$page = $this->pageExists($route, $slugs[$i]);
if (is_null($page)) {
if ($create) {
// Create folder if it doesn't exist
$slugs[$i] = $this->createFolder($path . DS . $slugs[$i]);
} else {
return null;
}
}
$route = $this->buildRoute($route, $page, $slugs[$i]);
$path = $this->buildPath($path, $page, $slugs[$i]);
}
}
}
return ['route' => $route, 'path' => $path];
}
/**
* Create a folder if it does not exist
*
* @param string $path
*
* @return string $folder_name
*/
public function createFolder($path)
{
$folder_names = explode(DS, $path);
$folder_name = $folder_names[count($folder_names) - 1];
if (!file_exists($path)) {
// split and sanitize leaf as a slug (disallowing periods)
$folder_name = $this->sanitize($folder_name, 'slug');
unset($folder_names[count($folder_names) - 1]);
$path = implode(DS, $folder_names);
Folder::create($path . DS . $folder_name);
}
return $folder_name;
}
/**
* Custom filter_var
*
* Based upon: https://gist.github.com/chtombleson/10002134
* Get a value from $_POST and sanitize it
*
* @param string $var Variable to check
* @param string $type What type is the value (string, email, int, float, encoded, url, email)
* @param array $option Options for filter_var
* @return mixed will return false on failure
*/
public function filter_post_var($var, $type = 'string', $options = array()) {
if (!isset($this->post[$var])) {
return false;
}
return filter_var($this->post[$var], $this->get_filter($type), $options);
}
/**
* Source: https://gist.github.com/chtombleson/10002134
*
*/
private function get_filter($type) {
switch (strtolower($type)) {
case 'string':
$filter = FILTER_SANITIZE_STRING;
break;
case 'int':
$filter = FILTER_SANITIZE_NUMBER_INT;
break;
case 'float' || 'decimal':
$filter = FILTER_SANITIZE_NUMBER_FLOAT;
break;
case 'encoded':
$filter = FILTER_SANITIZE_ENCODED;
break;
case 'url':
$filter = FILTER_SANITIZE_URL;
break;
case 'email':
$filter = FILTER_SANITIZE_EMAIL;
break;
default:
$filter = FILTER_SANITIZE_STRING;
}
return $filter;
}
/**
* Get parent page
*
* @param string $parent
* @param object $page
*
* @return object $parent_page
*/
public function getParentPage($parent, $page)
{
if ($parent != '') {
// Check for a relative parent
if ($parent[0] != DS) {
// Make an absolute route starting from this form page
$parent = $page->route() . DS . $parent;
}
// Check whether the parent page exists, allowing for modular pages
// but disallowing empty folders in the route
if (!is_null($this->crawlRoute($parent, false))) {
$parent_page = $this->grav['page']->find($parent);
} else {
// Gracefully continue and falback to adding the
// new page as a child page of the form page
$parent_page = $page;
}
} else {
$parent_page = $page;
}
return $parent_page; // can be a page or null if there is no page.
}
/**
* @return array
*
* The getSubscribedEvents() gives the core a list of events
* that the plugin wants to listen to. The key of each
* array section is the event that the plugin listens to
* and the value (in the form of an array) contains the
* callable (or function) as well as the priority. The
* higher the number the higher the priority.
*/
public static function getSubscribedEvents()
{
return [
'onPluginsInitialized' => ['onPluginsInitialized', 0],
'onFormProcessed' => ['onFormProcessed', 0]
];
}
/**
* @return string
*
* Return the state of a tristate configuration variable
*
*/
public function getTriStateConfig($value, $state) {
if(!isset($value)) {
return '';
}
if($value === $state) {
return $state;
}
if(gettype($value) === "boolean") {
if($value) {
return "true";
}
else {
return "false";
}
}
if(gettype($value) === "string") {
$value = trim(strtolower($value));
};
if(in_array($value, [1, '1', 'on', 'true'], true)) {
return 'true';
}
else {
return 'false';
}
}
/**
* Handle form action
*
* @param $event
*
*/
public function onFormProcessed(Event $event)
{
$form = $event['form'];
$action = $event['action'];
$params = $event['params'];
switch (true) {
case (in_array($action, $this->say_my_name)):
if (isset($_POST)) {
$pages = $this->grav['pages'];
$page = $this->grav['page'];
$header = $page->header();
$form_page_relative_page_path = $page->relativePagePath();
// Get default settings form plugin config
$include_username = $this->config->get('plugins.add-page-by-form.include_username');
$overwrite_mode = $this->getTriStateConfig($this->config->get('plugins.add-page-by-form.overwrite_mode'), 'edit');
$date_format = $this->config->get('plugins.add-page-by-form.date_display_format');
$auto_taxonomy_types = $this->config->get('plugins.add-page-by-form.auto_taxonomy_types');
$physical_template_name = $this->config->get('plugins.add-page-by-form.physical_template_name');
$slug_field = '';
// For next plugin version
$include_timestamp = false;
// Initialize default subroute
$sub_route = '';
// Get settings from pageconfig block and override values via form fields
if (isset($header->pageconfig) && is_array($header->pageconfig)) {
$positives = ['1', 'on', 'true'];
$pageconfig = $header->pageconfig;
if (isset($pageconfig['parent'])) {
$parent = strtolower(trim($pageconfig['parent']));
}
if (isset($pageconfig['subroute'])) {
$sub_route = strtolower(trim($pageconfig['subroute']));
}
if (isset($pageconfig['include_username'])) {
$include_username = in_array(strtolower(trim($pageconfig['include_username'])), $positives);
}
if (isset($pageconfig['overwrite_mode'])) {
$overwrite_mode = $this->getTriStateConfig($pageconfig['overwrite_mode'], 'edit');
}
if (isset($pageconfig['slug_field'])) {
$slug_field = strtolower(trim($pageconfig['slug_field']));
}
if (isset($pageconfig['physical_template_name'])) {
$physical_template_name = in_array(strtolower(trim($pageconfig['physical_template_name'])), $positives);
}
}
// Assemble the new page frontmatter from the page_frontmatter block
// as set in the form page
if (isset($header->pagefrontmatter) && is_array($header->pagefrontmatter)) {
$page_frontmatter = $header->pagefrontmatter;
} else {
$page_frontmatter = array();
}
// Add username (or not)
if ($include_username) {
$username = null;
if (!is_null($this->grav['session']->user)) {
$username = $this->grav['session']->user->username;
}
if (is_null($username)) {
$username = '';
}
$page_frontmatter['username'] = $username;
}
// Get all form field values
$form_data = $form->value()->toArray();
if (isset($form_data)) {
// Append taxonomy
if (isset($form_data['taxonomy']) && is_array($form_data['taxonomy'])) {
// Convert comma separated list into array assuming double quoted items
foreach ($form_data['taxonomy'] as $key => $value) {
$values = str_getcsv($value, ',', '"');
foreach ($values as $k => $v) {
$values[$k] = trim($v);
}
$form_data['taxonomy'][$key] = $values;
}
if (isset($page_frontmatter['taxonomy'])) {
// Append type/values
$page_frontmatter['taxonomy'] = array_merge_recursive($page_frontmatter['taxonomy'], $form_data['taxonomy']);
// Remove duplicate values
foreach ($page_frontmatter['taxonomy'] as $key => $value) {
if (is_array($page_frontmatter['taxonomy'][$key])) {
$page_frontmatter['taxonomy'][$key] = array_keys(array_flip($page_frontmatter['taxonomy'][$key]));
}
}
} else {
// Add taxonomy, types and values
$page_frontmatter['taxonomy'] = $form_data['taxonomy'];
}
// Remove taxonomy from form data (to prevent merging raw data)
unset($form_data['taxonomy']);
}
// Merge variables from pagefrontmatter block and form fields;
// Values that have been through a Twig Processor are in the
// page_frontmatter and take precedence over the form values
$page_frontmatter = array_merge($page_frontmatter, $form_data);
}
// Here you can insert anything else into the new page frontmatter
/*
$result = 'Hello World';
$page_frontmatter['result'] = $result;
*/
// If content is not included as a form value then fallback to config default
if (isset($page_frontmatter['content'])) {
$content = $page_frontmatter['content'];
} else {
$content = $this->config->get('plugins.add-page-by-form.default_content');
}
if ($physical_template_name && isset($page_frontmatter['template'])) {
$page_template = $page_frontmatter['template'];
// Remove the frontmatter variable template as which template
// must be used will be determined by the new page filename
unset($page_frontmatter['template']);
} else {
$page_template = 'default';
}
// Remove unwanted items from new page frontmatter
unset($page_frontmatter['_json']);
unset($page_frontmatter['content']);
unset($page_frontmatter['parent']);
unset($page_frontmatter['subroute']);
// Initialize default page parent
if (isset($header->parent)) {
// For backwards compatibility
$parent = $header->parent;
} else {
$parent = '';
}
// Override parent if set in pageconfig block
if (isset($pageconfig['parent'])) {
$parent = strtolower(trim($pageconfig['parent']));
}
// Override parent if set in the form
if (isset($form_data['parent'])) {
$parent = strtolower(trim($form_data['parent']));
}
// Removes multiple concatenated slashes plus a trailing slash if present
if (strlen($parent) > 1) {
$parent = preg_replace('/[\/]+/', DS, $parent);
$parent = rtrim($parent, DS);
}
// Get the "parent to be"
$parent_page = $this->getParentPage($parent, $page);
$parent_page_path = $parent_page->path();
$parent_page_route = $parent_page->route();
if ($overwrite_mode === 'edit') {
// Get slug of exisiting page
// Normal method
if(isset($form_data['edit_path'])) {
$slug = basename(dirname($form_data['edit_path']));
}
else {
// Alternative method
if(isset($form_data['file_path'])) {
$slug = basename(dirname($form_data['file_path']));
}
else {
$slug = '';
}
}
}
else {
// Create the slug for the new page
// Override subroute
if (isset($form_data['subroute'])) {
$sub_route = mb_strtolower(trim($form_data['subroute']));
}
if ($sub_route != '') {
// Remove any multiple concatenated slashes
$sub_route = preg_replace('/[\/]+/', DS, $sub_route);
// Remove preceding and trailing slashes if present
$sub_route = trim($sub_route, DS);
// Prepare for crawling
$parent_page_route = $parent_page->route() . DS . $sub_route;
// Create subroute path if it doesn't exist
$parent_destination = $this->crawlRoute($parent_page_route, true);
$parent_page_route = $parent_destination['route'];
$parent_page_path = $parent_destination['path'];
}
// Create a slug to be used as the page name (used publicly in URLs etc.)
if ($slug_field != '') {
if (isset($page_frontmatter[$slug_field])) {
$slug = self::slug($page_frontmatter[$slug_field]);
}
}
if (!isset($slug)) {
if (isset($page_frontmatter['title'])) {
$slug = self::slug($page_frontmatter['title']);
} else {
$slug = $this->config->get('plugins.add-page-by-form.default_title');
$slug = self::slug($slug);
}
}
}
if (empty($slug)) {
$this->grav->fireEvent('onFormValidationError', new Event([
'form' => $form,
'message' => '<strong>ERROR</strong> in Add Page by Form Plugin: Variable \'slug\' is empty']));
$event->stopPropagation();
return;
}
$new_page_folder = $parent_page_path . DS . $slug;
// Check overwrite mode
// When overwrite mode == 'true' replace page including media
// When overwrite mode == 'false' create a sequential named page
// When overwrite mode == 'edit' edit page and page media
if ($overwrite_mode !== 'false') {
if (file_exists($new_page_folder)) {
if ($overwrite_mode === 'edit') {
$original_frontmatter = (array)$pages->get($new_page_folder)->header();
}
else {
Folder::delete($new_page_folder);
}
}
} else {
// Scan for the next available sequential suffix
$version = 0;
// Keep incrementing the page slug suffix to keep earlier versions / duplicates
while (file_exists($new_page_folder)) {
$version += 1;
$new_page_folder = $parent_page_path . DS . $slug . '-' . $version;
}
if ($version > 0) {
$slug = $slug . '-' . $version;
}
}
// Create and add the page to Grav
try {
// Create new page
$new_page = new Page;
// Get active or default language for page filename
// (e.g. 'nl' -> 'default.nl.md')
$language = Grav::instance()['language']->getLanguage() ?: null;
$extension = '.md';
if ($language != '') {
$new_page_name = $page_template . '.' . $language . $extension;
} else {
$new_page_name = $page_template . $extension;
}
$path = $parent_page_path . DS . $slug . DS . $new_page_name;
$route = $parent_page_route . DS . $slug;
// Set page location vars
$new_page->name($new_page_name);
$new_page->folder($slug);
//$new_page->path($path);
$new_page->extension($extension);
$new_page->parent($parent_page);
$new_page->filePath($path);
$new_page->routable(true);
// Add frontmatter vars
$new_page->header((object) $page_frontmatter);
$new_page->frontmatter(Yaml::dump((array)$new_page->header(), 20));
// Set page markdown content vars
$new_page->rawMarkdown((string) $content);
$new_page->file()->markdown($new_page->rawMarkdown());
// Add routing information
$pages->addPage($new_page, $this->new_page_route);
// Fire BeforePageSave event
$this->grav->fireEvent('onAddPageByFormPluginBeforePageSave', new Event(['page' => &$new_page]));
// Set time vars
$new_page->modified(time());
// First page save (required to have an existing new page folder
// to store any files with destination '@self' in)
$new_page->save();
// Copy uploaded files to the new page folder
$copy_files = $this->copyFiles($form, $new_page);
// Get uploaded files plus properties
$file_fields = $copy_files['uploaded'];
// If original frontmatter exists update it to keep
// already present page media
if (isset($original_frontmatter)) {
$file_fields_updated = array();
foreach ($file_fields as $file_field => $uploads) {
$file_fields_updated[$file_field] = array_merge($original_frontmatter[$file_field], $uploads);
// Get any (uploaded and then) deleted files
foreach ($copy_files['deleted'] as $file_to_delete) {
if (file_exists($file_to_delete['file_path'])) {
unlink($file_to_delete['file_path']);
if (in_array($file_to_delete['file_path'], $file_fields_updated[$file_field][basename($file_to_delete['file_path'])])) {
unset($file_fields_updated[$file_field][basename($file_to_delete['file_path'])]);
}
}
}
}
$file_fields = $file_fields_updated;
}
// Add uploaded file properties to frontmatter
if (isset($file_fields) && isset($page_frontmatter)) {
$page_frontmatter = array_merge($page_frontmatter, $file_fields);
}
// Add modified frontmatter to the page header
$new_page->header((object) $page_frontmatter);
$new_page->frontmatter(Yaml::dump((array)$new_page->header(), 20));
// Set time vars
$new_page->modified(time());
// Update the new page
$new_page->save();
// Store the route so it can be used to redirect to if needed and to be saved as a Twig var
$this->new_page_route = $route;
$this->page_frontmatter = $page_frontmatter;
// Process any new taxonomy types
if ($auto_taxonomy_types && isset($page_frontmatter['taxonomy'])) {
// Read site configuration
$grav = Grav::instance();
$locator = $grav['locator'];
$filename = 'config://site.yaml';
$file = YamlFile::instance($locator->findResource($filename, true, true));
$site_config = Yaml::parse($file->load());
// Merge taxonomy types
$taxonomies = (array) $this->config->get('site.taxonomies');
foreach (array_keys($page_frontmatter['taxonomy']) as $type) {
$taxonomies = array_merge($taxonomies, (array) $type);
}
// Don't bother if there are no new taxonomy types
if (count(array_unique($taxonomies)) > count($site_config['taxonomies'])) {
$this->config->set('site.taxonomies', $taxonomies);
$taxonomies_merged = array();
$taxonomies_merged['taxonomies'] = array_values(array_unique($taxonomies));
$site_config = array_merge($site_config, $taxonomies_merged);
// Update taxonomy types in site.yaml
$file->save($site_config);
$file->free();
}
}
// Fire AfterPageSave event
$this->grav->fireEvent('onAddPageByFormPluginAfterPageSave', new Event(['page' => $new_page]));
} catch (\Exception $e) {
$this->grav['debugger']->addMessage($e->getMessage());
$this->grav->fireEvent('onFormValidationError', new Event([
'form' => $form,
'message' => '<strong>ERROR:</strong> ' . $e->getMessage()]));
$event->stopPropagation();
return;
}
}
break;
case ($action == 'redirect'):
// The Form plugin does not know how to handle '@self' as a redirect
// or display parameter, so prepare the redirect to the new page
switch (strtolower((string) $params)) {
case '@self':
$route = $this->new_page_route;
break;
case '@self-admin':
$admin_route = $this->config->get('plugins.admin.route');
if ($admin_route && $this->config->get('plugins.admin.enabled')) {
$base = DS . trim($admin_route, DS);
$route = $base . DS . 'pages' . $this->new_page_route;
} else {
// Admin not installed or inactive
// Fall back to @self
$route = $this->new_page_route;
}
break;
default:
// No valid redirect to self parameter
$route = '';
}
// Do the redirect
// BTW if there is no route the redirect is handed over to the Form plugin
if ($route) {
/** @var Twig $twig */
$twig = $this->grav['twig'];
$twig->twig_vars['form'] = $form;
$twig->twig_vars['pagefrontmatter'] = $this->page_frontmatter;
/** @var Pages $pages */
$pages = $this->grav['pages'];
$page = $pages->dispatch($route, false);
// Redirect to the new page
unset($this->grav['page']);
$this->grav['page'] = $page;
$this->grav->redirect($route);
}
break;
}
}
/**
* Add assets
*
*/
public function onPageInitialized()
{
// Some forms (like forgot_password) do not have frontmatter
if (null !== ($this->grav['page']->frontmatter())) {
$data = Yaml::parse($this->grav['page']->frontmatter());
// Only act upon forms which are intended to be processed by this plugin
if (isset($data['form']) && isset($data['form']['name']) &&
in_array(strtolower(substr($data['form']['name'], 0, 7)), $this->say_my_name)) {
if ($this->config->get('plugins.add-page-by-form.use_editor_class',true)) {
$assets = $this->grav['assets'];
// Add jQuery library (no harm done when already present)
$assets->add('jquery', 101);
// Add SimpleMDE Markdown Editor
$assets->addCss('//cdn.jsdelivr.net/simplemde/latest/simplemde.min.css', 100);
$assets->addJs('//cdn.jsdelivr.net/simplemde/latest/simplemde.min.js', 100);
// Add custom styles
$assets->addCss('plugin://add-page-by-form/assets/css/customstyles.css', 110);
// Load inline Javascript code from configuration file
$assets->addInlineJs(file_get_contents('plugin://add-page-by-form/assets/js/simplemde_config.js'), 110);
}
}
}
}
/**
* Initialize the plugin
*/
public function onPluginsInitialized()
{
// Don't proceed if we are in the admin plugin
if ($this->isAdmin()) {
return;
}
// Enable the events we are interested in
$this->enable([
'onPageInitialized' => ['onPageInitialized', 0],
]);
}
/**
* Check whether a page exists at the specified route irrespective of page type
*
* @param string $route
* @param string $slug
*
* @return object $page
*/
public function pageExists($route, $slug)
{
$page = $this->grav['page']->find($route . DS . $slug);
if (is_null($page) && !empty($slug) && $slug[0] != '_') {
$page = $this->grav['page']->find($route . DS . '_' . $slug);
}
return $page;
}
/**
* Generates a slug of the given string
* Source: Laravel str_slug()
*
* @param string $str
* @param string $separator (optional)
* @return string
*/
public static function slug(string $str, string $separator = '-')
{
// Sanitize non-latin characters
$str = self::sanitize($str, 'slug');
// Convert all dashes/underscores into separator
$flip = $separator === '-' ? '_' : '-';
$str = preg_replace('!['.preg_quote($flip).']+!u', $separator, $str);
// Replace @ with the word 'at'
$str = str_replace('@', $separator.'at'.$separator, $str);
// Remove all characters that are not the separator, letters, numbers, or whitespace.
$str = preg_replace('![^'.preg_quote($separator).'\pL\pN\s]+!u', '', strtolower($str));
// Replace all separator characters and whitespace by a single separator
$str = preg_replace('!['.preg_quote($separator).'\s]+!u', $separator, $str);
return trim($str, $separator);
}
/**
* Sanitize a string into a safe filename or slug
*
* @param string $f
*
* @return string
*/
public function sanitize($f, $type = 'file')
{
/* A combination of various methods to sanitize a string while retaining
the "essence" of the original file name as much as possible.
Note: unsuitable for file paths as '/' and '\' are filtered out.
Sources:
http://www.house6.com/blog/?p=83
and
http://stackoverflow.com/a/24984010
*/
$replace_chars = array(
'&amp;' => '-and-', '@' => '-at-', '©' => 'c', '®' => 'r', 'À' => 'a',
'Á' => 'a', 'Â' => 'a', 'Ä' => 'a', 'Å' => 'a', 'Æ' => 'ae', 'Ç' => 'c',
'È' => 'e', 'É' => 'e', 'Ë' => 'e', 'Ì' => 'i', 'Í' => 'i', 'Î' => 'i',
'Ï' => 'i', 'Ò' => 'o', 'Ó' => 'o', 'Ô' => 'o', 'Õ' => 'o', 'Ö' => 'o',
'Ø' => 'o', 'Ù' => 'u', 'Ú' => 'u', 'Û' => 'u', 'Ü' => 'u', 'Ý' => 'y',
'ß' => 'ss', 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ä' => 'a', 'å' => 'a',
'æ' => 'ae', 'ç' => 'c', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',
'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ò' => 'o', 'ó' => 'o',
'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ø' => 'o', 'ù' => 'u', 'ú' => 'u',
'û' => 'u', 'ü' => 'u', 'ý' => 'y', 'þ' => 'p', 'ÿ' => 'y', 'Ā' => 'a',
'ā' => 'a', 'Ă' => 'a', 'ă' => 'a', 'Ą' => 'a', 'ą' => 'a', 'Ć' => 'c',
'ć' => 'c', 'Ĉ' => 'c', 'ĉ' => 'c', 'Ċ' => 'c', 'ċ' => 'c', 'Č' => 'c',
'č' => 'c', 'Ď' => 'd', 'ď' => 'd', 'Đ' => 'd', 'đ' => 'd', 'Ē' => 'e',
'ē' => 'e', 'Ĕ' => 'e', 'ĕ' => 'e', 'Ė' => 'e', 'ė' => 'e', 'Ę' => 'e',
'ę' => 'e', 'Ě' => 'e', 'ě' => 'e', 'Ĝ' => 'g', 'ĝ' => 'g', 'Ğ' => 'g',
'ğ' => 'g', 'Ġ' => 'g', 'ġ' => 'g', 'Ģ' => 'g', 'ģ' => 'g', 'Ĥ' => 'h',
'ĥ' => 'h', 'Ħ' => 'h', 'ħ' => 'h', 'Ĩ' => 'i', 'ĩ' => 'i', 'Ī' => 'i',
'ī' => 'i', 'Ĭ' => 'i', 'ĭ' => 'i', 'Į' => 'i', 'į' => 'i', 'İ' => 'i',
'ı' => 'i', 'IJ' => 'ij', 'ij' => 'ij', 'Ĵ' => 'j', 'ĵ' => 'j', 'Ķ' => 'k',
'ķ' => 'k', 'ĸ' => 'k', 'Ĺ' => 'l', 'ĺ' => 'l', 'Ļ' => 'l', 'ļ' => 'l',
'Ľ' => 'l', 'ľ' => 'l', 'Ŀ' => 'l', 'ŀ' => 'l', 'Ł' => 'l', 'ł' => 'l',
'Ń' => 'n', 'ń' => 'n', 'Ņ' => 'n', 'ņ' => 'n', 'Ň' => 'n', 'ň' => 'n',
'ʼn' => 'n', 'Ŋ' => 'n', 'ŋ' => 'n', 'Ō' => 'o', 'ō' => 'o', 'Ŏ' => 'o',
'ŏ' => 'o', 'Ő' => 'o', 'ő' => 'o', 'Œ' => 'oe', 'œ' => 'oe', 'Ŕ' => 'r',
'ŕ' => 'r', 'Ŗ' => 'r', 'ŗ' => 'r', 'Ř' => 'r', 'ř' => 'r', 'Ś' => 's',
'ś' => 's', 'Ŝ' => 's', 'ŝ' => 's', 'Ş' => 's', 'ş' => 's', 'Š' => 's',
'š' => 's', 'Ţ' => 't', 'ţ' => 't', 'Ť' => 't', 'ť' => 't', 'Ŧ' => 't',
'ŧ' => 't', 'Ũ' => 'u', 'ũ' => 'u', 'Ū' => 'u', 'ū' => 'u', 'Ŭ' => 'u',
'ŭ' => 'u', 'Ů' => 'u', 'ů' => 'u', 'Ű' => 'u', 'ű' => 'u', 'Ų' => 'u',
'ų' => 'u', 'Ŵ' => 'w', 'ŵ' => 'w', 'Ŷ' => 'y', 'ŷ' => 'y', 'Ÿ' => 'y',
'Ź' => 'z', 'ź' => 'z', 'Ż' => 'z', 'ż' => 'z', 'Ž' => 'z', 'ž' => 'z',
'ſ' => 'z', 'Ə' => 'e', 'ƒ' => 'f', 'Ơ' => 'o', 'ơ' => 'o', 'Ư' => 'u',
'ư' => 'u', 'Ǎ' => 'a', 'ǎ' => 'a', 'Ǐ' => 'i', 'ǐ' => 'i', 'Ǒ' => 'o',
'ǒ' => 'o', 'Ǔ' => 'u', 'ǔ' => 'u', 'Ǖ' => 'u', 'ǖ' => 'u', 'Ǘ' => 'u',
'ǘ' => 'u', 'Ǚ' => 'u', 'ǚ' => 'u', 'Ǜ' => 'u', 'ǜ' => 'u', 'Ǻ' => 'a',
'ǻ' => 'a', 'Ǽ' => 'ae', 'ǽ' => 'ae', 'Ǿ' => 'o', 'ǿ' => 'o', 'ə' => 'e',
'Ё' => 'jo', 'Є' => 'e', 'І' => 'i', 'Ї' => 'i', 'А' => 'a', 'Б' => 'b',
'В' => 'v', 'Г' => 'g', 'Д' => 'd', 'Е' => 'e', 'Ж' => 'zh', 'З' => 'z',
'И' => 'i', 'Й' => 'j', 'К' => 'k', 'Л' => 'l', 'М' => 'm', 'Н' => 'n',
'О' => 'o', 'П' => 'p', 'Р' => 'r', 'С' => 's', 'Т' => 't', 'У' => 'u',
'Ф' => 'f', 'Х' => 'h', 'Ц' => 'c', 'Ч' => 'ch', 'Ш' => 'sh', 'Щ' => 'sch',
'Ъ' => '-', 'Ы' => 'y', 'Ь' => '-', 'Э' => 'je', 'Ю' => 'ju', 'Я' => 'ja',
'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e',
'ж' => 'zh', 'з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k', 'л' => 'l',
'м' => 'm', 'н' => 'n', 'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's',
'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c', 'ч' => 'ch',
'ш' => 'sh', 'щ' => 'sch', 'ъ' => '-', 'ы' => 'y', 'ь' => '-', 'э' => 'je',
'ю' => 'ju', 'я' => 'ja', 'ё' => 'jo', 'є' => 'e', 'і' => 'i', 'ї' => 'i',
'Ґ' => 'g', 'ґ' => 'g', 'א' => 'a', 'ב' => 'b', 'ג' => 'g', 'ד' => 'd',
'ה' => 'h', 'ו' => 'v', 'ז' => 'z', 'ח' => 'h', 'ט' => 't', 'י' => 'i',
'ך' => 'k', 'כ' => 'k', 'ל' => 'l', 'ם' => 'm', 'מ' => 'm', 'ן' => 'n',
'נ' => 'n', 'ס' => 's', 'ע' => 'e', 'ף' => 'p', 'פ' => 'p', 'ץ' => 'C',
'צ' => 'c', 'ק' => 'q', 'ר' => 'r', 'ש' => 'w', 'ת' => 't', '™' => 'tm',
'Ã' => 'A', 'Ð' => 'Dj', 'Ê' => 'E', 'Ñ' => 'N', 'Þ' => 'B', 'ã' => 'a',
'ð' => 'o', 'ñ' => 'n', '#' => '-nr-');
// "Translate" multi byte characters to 'corresponding' ASCII characters
$f = strtr($f, $replace_chars);
// Convert special characters to a hyphen
$f = str_replace(array(
' ', '!', '\\', '/', '\'', '`', '"', '~', '%', '|',
'*', '$', '^', '(', ')', '[', ']', '{', '}',
'+', ',', ':', ';', '<', '=', '>', '?', '|'), '-', $f);
// Remove any non ASCII characters
$f = preg_replace('/[^(\x20-\x7F)]*/', '', $f);
if ($type == 'file') {
// Remove non-word chars (leaving hyphens and periods)
$f = preg_replace('/[^\w\-\.]+/', '', $f);
// Convert multiple adjacent dots into a single one
$f = preg_replace('/[\.]+/', '.', $f);
} else { // Do not allow periods, for instance for a Grav slug
// Convert period to hyphen
$f = str_replace('.', '-', $f);
// Remove non-word chars (leaving hyphens)
$f = preg_replace('/[^\w\-]+/', '', $f);
}
// Convert multiple adjacent hyphens into a single one
$f = preg_replace('/[\-]+/', '-', $f);
// Change into a lowercase string; BTW no need to use mb_strtolower() here ;)
$f = strtolower($f);
return $f;
}
}