<?php
/*
Plugin Name: Permalinks
Description: Create SEO optimized permalinks, custom URLS, or custom aliases for any page.
Version: 1.05
Requires at least: 3.00
*/

### UPDATE THESE VALUES
$GLOBALS['PERMALINKS']['autopopulate']            = true; // automatically fill in permalink field if user leaves it blank
$GLOBALS['PERMALINKS']['autopopulate_fromFields'] = array('name', 'title', 'username', 'createdDate'); // the first field in this list with content will be used to create the permalinks. To use multiple fields separate them with spaces
$GLOBALS['PERMALINKS']['autopopulate_skipWords']  = array('a','an','and','as','at','before','but','by','for','from','is','in','into','it','like','of','off','on','onto','per','since','than','the','this','that','to','up','via','with'); // these seo unfriendly words will be removed
$GLOBALS['PERMALINKS']['404_not_found_filepath']  = dirname(__FILE__) . '/permalinks_notFound404.php'; // filepath to include if Permalink isn't found

// DON'T UPDATE ANYTHING BELOW THIS LINE

$GLOBALS['PERMALINKS_PLUGIN']         = true;
$GLOBALS['PERMALINKS_HAS_REDIRECTED'] = false;

// Viewers
addFilter('viewer_output_rows',            'permalink_viewers_updateLinks',         null, 3);

// CMS Menus - Functions & Plugin Hooks
if (defined('IS_CMS_ADMIN')) { 
  require_once("permalinks_cmsMenus.php");
}

// Code Generator
addGenerator('permalink_codeGenerator',  t("Permalinks"), t("Generate .htaccess code for redirecting requests."));
function permalink_codeGenerator() { require_once("permalinks_codeGenerator.php"); permalink_codeGenerator_showPage(); exit; }

// Plugin Menu 
if (defined('IS_CMS_ADMIN') && (@$_REQUEST['action'] == 'plugins' || @$_REQUEST['_pluginAction'])) { // if in CMS Plugins Menu
  require_once("permalinks_pluginsMenu.php");
  
  pluginAction_addHandlerAndLink(t('Code Generator'), 'permalink_pluginMenu_codeGeneratorLink', 'admins');
  pluginAction_addHandlerAndLink(t('Permalinks DB'), 'permalink_pluginMenu_permalinksMenuLink', 'admins');
  pluginAction_addHandlerAndLink(t('Update Tables'), 'permalink_pluginMenu_updateTable', 'admins');
  pluginAction_addHandler('_permalink_pluginMenu_updateTable_ajax', 'admins');
  
  // Add "Debug Output" link
  addAction('plugin_actions', '_permalink_debugOutput_link');
  function _permalink_debugOutput_link($menuPluginPage) {
    static $thisPluginPath; 
    if (!$thisPluginPath) { list($thisPluginPath) = getPluginPathAndUrl(); }
    if ($thisPluginPath != $menuPluginPage) { return; }
    print "<a href='/non-existant-url?this=that' target='_blank'>Debug Output</a>";
  } 
  
  // Add *Internal Testplan* link: 
  list($testPlanPath, $testPlanURL) = getPluginPathAndUrl('_INTERNAL_testplan.php');
  if (file_exists("plugins/$testPlanPath")) {
    function permalink_runTestplan() {
      list($testPlanPath, $testPlanURL) = getPluginPathAndUrl('_INTERNAL_testplan.php');
      redirectBrowserToURL($testPlanURL);
    }
    pluginAction_addHandlerAndLink('*Internal Testplan*', 'permalink_runTestplan', 'admins');
  }
  
}

// -------------------------------------------

// Redirect non-permalink urls to permalinks: 
// Usage: permalinks_301redirectToPermalink();        // for list viewer pages, checks if current url matches any custom permalinks and redirects if needed
// Usage: permalinks_301redirectToPermalink($record); // for detail viewer pages, checks for record's table and num in permalinks db and redirects if needed
function permalinks_301redirectToPermalink($record = null) {
  if (!array_key_exists('PERMALINKS_HAS_REDIRECTED', $GLOBALS)) { return; } // this should always to defined by plugin (starts as false)
  if ($GLOBALS['PERMALINKS_HAS_REDIRECTED']) { return; }                    // skip if already redirected
  $latestLink = '';
  
  // redirect records
  if ($record && @$record['_tableName'] && @$record['num'] && @$record['permalink']) {
    // NOTE: A future optimization could be to just redirect to $record['_link'] when it's defined (although it's not set by mysql_get/select),
    // ... however, in a best-case of old-urls not being used and being updated by search engines this code won't be called much.
    $whereEtc   = mysql_escapef("`tableName` = ? AND `recordNum` = ? AND `old` = '0'", $record['_tableName'], $record['num']);
    $latestLink = mysql_get('_permalinks', null, $whereEtc);
  }
  
  // redirect urls
  if (is_null($record)) {
    $permalinkWhere  = array('customSourceUrl' => $_SERVER['REQUEST_URI'], 'old' => '0');
    $latestLink      = mysql_get('_permalinks', null, $permalinkWhere);
  }

  // redirect
  if (@$latestLink['permalink']) { 
    header("Location: " .PREFIX_URL. "/{$latestLink['permalink']}/", true, 301); // 301 Moved Permanently
    exit;
  }
}

// return permalink or blank on fail
// prefix can be found in $GLOBALS['schema']['permalink']['defaultValue'] or $tableSchema['permalink']['defaultValue']
function permalink_generate($recordOrRequest, $prefixPermalinkWith = '') {
  $uniquePermalink = '';
  
  $inputText         = _permalink_generate_getInputText($recordOrRequest);  // get input text from record fields
  $permalinkText     = _permalink_generate_formatInputText($inputText);     // convert input text to permalink
  if ($permalinkText) { 
    $prefixedPermalink = $prefixPermalinkWith . $permalinkText;               // add prefix
    $uniquePermalink   = $prefixedPermalink . _permalink_generate_getUniqueSuffix($prefixedPermalink, $recordOrRequest['num']);  // check for duplicates and get unique extension
  }
  
  //
  return $uniquePermalink;
}

// merge input text from one or more input fields, return blank if no populated field combinations or fields found
function _permalink_generate_getInputText($recordOrRequest) {

  foreach ($GLOBALS['PERMALINKS']['autopopulate_fromFields'] as $spaceSeparatedFieldSet) {
    $fieldSet  = array_filter(explode(' ', $spaceSeparatedFieldSet));
    $inputText = '';
    foreach ($fieldSet as $field) { // add text from each field in field set
      if (!array_key_exists($field, $recordOrRequest)) { continue 2; } // skip if one or more fields not defined
      if($field == 'createdDate'){
        $recordOrRequest[$field] = strtotime($recordOrRequest[$field]) + $recordOrRequest['num'];
      }

      $inputText .= $recordOrRequest[$field] . ' '; // trailing space in case more fields are added
    }
    break;
  }
  $inputText = rtrim($inputText, ' '); // remove trailing spaces
  
  return $inputText;
}

// Return permalinkText with SEO rules applied
// SEO Rules include: remove "skip words", spaces to dashes, remove non-alphanumeric, remove leading/trailing underscores
// returns blank if no permalink could be generated (such as if title was all spaces)
function _permalink_generate_formatInputText($inputText) {
  $permalinkText = $inputText;

  // remove accents from latin chars for SEO and readability - ref: https://www.google.com/search?q=seo+accented+characters
  static $charsToEntities = array( // reference: http://php.net/manual/en/function.str-replace.php#85431
    'A'   => '/&Agrave;|&Aacute;|&Acirc;|&Atilde;|&Auml;|&Aring;/',
    'a'   => '/&agrave;|&aacute;|&acirc;|&atilde;|&auml;|&aring;/',
    'C'   => '/&Ccedil;/',
    'c'   => '/&ccedil;/',
    'E'   => '/&Egrave;|&Eacute;|&Ecirc;|&Euml;|&AElig;/',
    'e'   => '/&egrave;|&eacute;|&ecirc;|&euml;|&aelig;/',
    'I'   => '/&Igrave;|&Iacute;|&Icirc;|&Iuml;/',
    'i'   => '/&igrave;|&iacute;|&icirc;|&iuml;/',
    'N'   => '/&Ntilde;/',
    'n'   => '/&ntilde;/',
    'O'   => '/&Ograve;|&Oacute;|&Ocirc;|&Otilde;|&Ouml;|&Oslash;/',
    'o'   => '/&ograve;|&oacute;|&ocirc;|&otilde;|&ouml;|&oslash;/',
    'U'   => '/&Ugrave;|&Uacute;|&Ucirc;|&Uuml;/',
    'u'   => '/&ugrave;|&uacute;|&ucirc;|&uuml;/',
    'Y'   => '/&Yacute;/',
    'y'   => '/&yacute;|&yuml;/',
    'a.'  => '/&ordf;/',
    'o.'  => '/&ordm;/',
    'and' => '/&amp;/',
  );
  $permalinkText = htmlentities($permalinkText, ENT_NOQUOTES, 'UTF-8'); // convert special chars to html entities
  $permalinkText = preg_replace($charsToEntities, array_keys($charsToEntities), $permalinkText); // replace those entities with non accented versions

  // remove skip words
  static $skipWordsRegexp = '';
  if (!$skipWordsRegexp) { $skipWordsRegexp = implode('|', array_filter($GLOBALS['PERMALINKS']['autopopulate_skipWords'], 'preg_quote')); }
  $permalinkText = preg_replace("/\b($skipWordsRegexp)\b/i", '', $permalinkText); // remove "skip words"
  
  // format permalink  
  $permalinkText = preg_replace('/\s+/', '-', $permalinkText);               // replace spaces with dashes
  $permalinkText = preg_replace('/[^a-z0-9\-\.\_]+/i', '', $permalinkText);  // remove non-alphanumeric chars
  $permalinkText = preg_replace("/-+/", '-', $permalinkText);                // collapse multiple dashes
  $permalinkText = preg_replace("/(^-+|-+$)/", '', $permalinkText);          // remove leading and trailing underscores
  $permalinkText = strtolower($permalinkText);                               // lowercase permalink
  
  //
  return $permalinkText;
}

//
function _permalink_generate_getUniqueSuffix($permalinkTextWithPrefix, $recordNum) {
  global $TABLE_PREFIX;
  $suffix = '';

  // Check if permalink is in use
  $countWhere     = "`permalink` = '" .mysql_escape($permalinkTextWithPrefix). "' "; 
  if ($recordNum) { $countWhere .= " AND `recordNum` != '" .mysql_escape($recordNum). "'"; } // skip current record in case we're saving an existing permalink
  $permalinkInUse = mysql_count('_permalinks', $countWhere);
  
  // get unique suffix if permalink in use
  if ($permalinkInUse) {
    // get list of permalinks that have this permalink as a prefix (this will match permalink-1, permalink-2, but also permalinkSomethingElse)
    $where   = "WHERE `permalink` LIKE '" .mysql_escape($permalinkTextWithPrefix, true). "%' "; // find permalinks that start with the same string as this one
    if ($recordNum) { $where .= " AND `recordNum` != '" .mysql_escape($recordNum). "'"; } // skip current record in case we're saving an existing permalink
    $records = mysql_select_query("SELECT permalink FROM {$TABLE_PREFIX}_permalinks $where");  
    $matchingPermalinks = array_pluck($records, 'permalink');

    // find unused suffix
    $suffixNum = 1;
    while (in_array("$permalinkTextWithPrefix-$suffixNum", $matchingPermalinks)) {
      $suffixNum++;
    }
    $suffix = "-$suffixNum";  
  }
  
  //
  return $suffix;  
}


// Add/Update permalink for record and set all records previous permalinks as 'old'.  Returns error if
// ... permalink already used by another record NOTE: This function DOES NOT do any error checking
// Usage: $errors = permalink_add($permalink, $tableName, $recordNum);
function permalink_add($permalinkText, $tableName, $recordNum, $updateRecordTable = false) {
  $errors = '';
  
  // check if permalink in use
  $permalinkRecord = mysql_get('_permalinks', null, array('permalink' => $permalinkText)); // check if permalink already exists in database
  $isAlreadyUsed   = $permalinkRecord && $permalinkRecord['recordNum'] != $recordNum;
  if ($isAlreadyUsed) {
    $errors = "Error: Permalink is already in use, please choose another!";
  }

  //
  if (!$errors) { 
    // mark all other matching permalinks for this record as old
    $where = mysql_escapef("`tableName` = ? AND `recordNum` = ? AND permalink != ?", $tableName, $recordNum, $permalinkText);
    mysql_update('_permalinks', null, $where, array('old' => '1'));
  
    // insert/update
    $colsToValues = array('permaLink' => $permalinkText, 'tableName' => $tableName, 'recordNum' => $recordNum, 'updatedDate=' => 'NOW()', 'old' => '0');
    if ($permalinkRecord) { mysql_update('_permalinks', $permalinkRecord['num'], null, $colsToValues); }
    else                  { mysql_insert('_permalinks', $colsToValues); }

    if($updateRecordTable){
      mysql_update($tableName, $recordNum, null, array('permalink' => $permalinkText));
    }
  }

  //
  return $errors;  
}



//
function permalink_viewers_updateLinks($rows, $listDetails, $schema) {
  if (!@array_key_exists('permalink', $schema)) { return $rows; } // skip if no permalink field specified

  // update _link to point to permalink
  foreach (array_keys($rows) as $index) {
    $row = &$rows[$index];
    if ($row['permalink']) { $row['_link'] = PREFIX_URL . '/' .$row['permalink']. '/'; }
    unset($row);
  }

  //
  return $rows;
}

// list($abspath, $pathErrors) = _permalink_getAbsolutePath($tablename, null);
// list($abspath, $pathErrors) = _permalink_getAbsolutePath(null, $customSourceUrl);
// if ($pathErrors) { die( nl2br(htmlencode($pathErrors)) ); }
function _permalink_getAbsolutePath($tableName = null, $relPathAndQuery = null) {
  global $SETTINGS;
  $absPath = '';
  $errors  = '';

  // error checking
  if (!file_exists($SETTINGS['webRootDir'])) {
    $errors .= sprintf(t("Check Settings, the following 'Website Root Directory' is invalid: '%s'"), $SETTINGS['webRootDir']) . "\n";
    return(array(null, $errors));
  }

  // custom source url
  if ($relPathAndQuery) {
    $relPath = array_first(explode('?', $relPathAndQuery));       // remove ?query=string from viewerUrl (if any)
    $absPath = absPath(ltrim($relPath,'/'), $SETTINGS['webRootDir']);
    if (!file_exists($absPath)) {
      $errors .= sprintf(t("Couldn't find filepath for permalink 'Custom Source Url' of '%s'!"), $relPath) . "\n";
      $errors .= t("Check that file exists and 'Website Root Directory' is correct under settings.") . "\n";
    }
    elseif (!file_exists($absPath))       { $errors .= sprintf(t("Permalink filepath does not exist '%s'!"), $absPath) . "\n"; }
  }

  // automatic permalinks
  elseif ($tableName) {
    $schema        = loadSchema( $tableName );
    $viewerUrl     = $schema['_detailPage'];
    $viewerRelPath = array_value(explode('?', $viewerUrl), 0);       // remove ?query=string from viewerUrl (if any)
    $viewerRelPath = str_replace('%20', ' ', $viewerRelPath);        // convert url encoded %20 to file-system friendly ' '
    if (file_exists($SETTINGS['webRootDir'].PREFIX_URL.$viewerRelPath)) { $viewerRelPath = PREFIX_URL.$viewerRelPath; } // add PREFIX_URL if file exists (/~username/ prefixs may be virtual only and not reflected in filesystem paths, where-as /clients/example.com/ prefixes may exist in filepaths)
    $absPath       = absPath(ltrim($viewerRelPath,'/'), $SETTINGS['webRootDir']);

    if     (!$viewerUrl)                  { $errors .= sprintf(t("No 'Detail Page URL' set under: Admin > Section Editors > '%s' > Viewer Urls!"), $tableName). "\n"; }
    elseif (contains('http', $viewerUrl)) { $errors .= sprintf(t("'Detail Page URL' can't start with http in: Admin > Section Editors > '%s' > Viewer Urls!"), $tableName). "\n"; }
    elseif (!$viewerRelPath)              { $errors .= sprintf(t("'Detail Page URL' doesn't contain a filename under: Admin > Section Editors > '%s' > Viewer Urls!"), $tableName). "\n"; }
    elseif (!$absPath)                    { $errors .= sprintf(t("Couldn't find filepath for 'Detail Page URL' under: Admin > Section Editors > '%s' > Viewer Urls! Relative path: %s"), $tableName, $viewerRelPath). "\n"; }
    elseif (!file_exists($absPath))       { $errors .= sprintf(t("Permalink filepath does not exist '%s'!"), $absPath) . "\n"; }
  }

  //
  return array($absPath, $errors);
}


// get original url user was redirect to permalinks plugin from
function _permalink_getRedirectUrl() {

  // NOTE: mod_rewrite adds keys to $_SERVER prefixed with: REDIRECT_
  // NOTE: For servers that don't set REDIRECT_URL we can check SCRIPT_URL AND REQUEST_URI (reverse order if needed because of other servers settings vars differently).

  // old code: 
  // $redirectUrl = coalesce(array_first(explode('?',$_SERVER['REQUEST_URI'])), @$_SERVER['REDIRECT_URL'], @$_SERVER['SCRIPT_URL'] );
  
  // get permalinks plugin url
  $adminDirUrl    = dirname(parse_url($GLOBALS['SETTINGS']['adminUrl'], PHP_URL_PATH));                 // eg: /cmsb
  $pluginFilepath = str_replace('\\', '/', __FILE__);                                                   // eg: C:/www/cmsb/plugins/permalinks/permalinks.php
  $pluginUrlPath  = preg_replace("|^.*[\\\/]plugins[\\\/]|", "$adminDirUrl/plugins/", $pluginFilepath); // eg: /cmsb/plugins/permalinks/permalinks.php
  // For future: We might want to compare server values against this to disregard them.
  
  // get redirect url
  $redirectUrl = '';
  if     (@$_SERVER['REQUEST_URI'])        { $redirectUrl = $_SERVER['REQUEST_URI']; }
  elseif (@$_SERVER['REDIRECT_URL'])       { $redirectUrl = $_SERVER['REDIRECT_URL']; }
  elseif (@$_SERVER['SCRIPT_URL'])         { $redirectUrl = $_SERVER['SCRIPT_URL']; }
  elseif (@$_SERVER['HTTP_X_REWRITE_URL']) { $redirectUrl = $_SERVER['HTTP_X_REWRITE_URL']; }  // For Windows/IIS Servers with ISAPI_Rewrite
                          
  // remove prefix url, these are sometimes used for preview domains or dev server urls eg: /~username or /development/client-name/
  $redirectUrl = str_replace(PREFIX_URL, '', $redirectUrl);
  
  // remove query string
  $redirectUrl = preg_replace("/\?.*/", '', $redirectUrl);
  
  //
  return $redirectUrl;
}

// eof