# PLUGIN PREVIEW BY TEXTPATTERN.INFO
/**
* smd_access_keys
*
* A Textpattern CMS plugin for secure tokenized access to resources. Features:
* -> Time-based or access attempt limits
* -> Untamperable URL-based keys
* -> Optional IP logging
*
* @author Stef Dawson
* @link http://stefdawson.com/
* @todo Add shortcut to send a newly-generated admin-side key to a user? (dropdown of users + e-mail template in prefs?)
* @todo Add auto-deletion of expired keys pref:
* -> File download keys can be deleted on any key access (expiry window is known via prefs).
* -> Other key accesses can only be deleted when that key is used.
* -> Configurable grace period after expiry, before deletion.
* @todo Obfusctaed URLs?
* @todo Query an access key and separate it into its component parts for convenient testing.
*/
if (txpinterface === 'admin') {
global $smd_akey_event, $smd_akey_styles, $dbversion;
$smd_akey_event = 'smd_akey';
$smd_akey_styles = array(
'list' =>
'.smd_hidden { display:none; }',
);
if (version_compare($dbversion, '4.6-dev') >= 0) {
add_privs('prefs.smd_akey', '1,2,3');
}
add_privs($smd_akey_event, '1');
add_privs('plugin_prefs.smd_access_keys', '1');
register_tab('extensions', $smd_akey_event, gTxt('smd_akey_tab_name'));
register_callback('smd_akey_dispatcher', $smd_akey_event);
register_callback('smd_akey_welcome', 'plugin_lifecycle.smd_access_keys');
register_callback('smd_akey_prefs', 'plugin_prefs.smd_access_keys');
}
/**
* Keys used on the prefs screen.
*
* Displayed this way to help ied_plugin_composer find them:
* gTxt('smd_akey')
* gTxt('smd_akey_file_download_expires')
* gTxt('smd_akey_salt_length')
* gTxt('smd_akey_log_ip')
*/
global $smd_akey_prefs;
$smd_akey_prefs = array(
'smd_akey_file_download_expires' => array(
'html' => 'text_input',
'type' => PREF_HIDDEN,
'position' => 10,
'default' => '3600',
),
'smd_akey_salt_length' => array(
'html' => 'text_input',
'type' => PREF_HIDDEN,
'position' => 20,
'default' => '8',
),
'smd_akey_log_ip' => array(
'html' => 'yesnoradio',
'type' => PREF_HIDDEN,
'position' => 30,
'default' => '0',
),
);
if (!defined('SMD_AKEYS')) {
define("SMD_AKEYS", 'smd_akeys');
}
register_callback('smd_access_protect_download', 'file_download');
/**
* ADMIN SIDE INTERFACE
* ====================
*
* Jump off point for event/steps.
*
* @param string $evt (req) Textpattern event
* @param string $stp (req) Textpattern step
*/
function smd_akey_dispatcher($evt, $stp)
{
if (!$stp or !in_array($stp, array(
'smd_akey_table_install',
'smd_akey_table_remove',
'smd_akey_create',
'smd_akey_prefs',
'smd_akey_prefsave',
'smd_akey_multi_edit',
'smd_akey_change_pageby',
))) {
smd_akey('');
} else $stp();
}
/**
* Bootstrap when plugin installed/deleted.
*
* @param string $evt (req) Textpattern event
* @param string $stp (req) Textpattern step
* @return string Notification message
*/
function smd_akey_welcome($evt, $stp)
{
$msg = '';
switch ($stp) {
case 'installed':
smd_akey_table_install(0);
$msg = 'Restrict your Txp world :-)';
break;
case 'deleted':
smd_akey_table_remove(0);
break;
}
return $msg;
}
/**
* Main admin interface
*
* @param string $msg (opt) Flash status message to display
* @return HTML Page content
*/
function smd_akey($msg = '')
{
global $smd_akey_event, $smd_akey_list_pageby, $smd_akey_styles, $logging, $smd_akey_prefs;
pagetop(gTxt('smd_akey_tab_name'), $msg);
if (smd_akey_table_exist(1)) {
extract(gpsa(array('page', 'sort', 'dir', 'crit', 'search_method')));
if ($sort === '') $sort = get_pref('smd_akey_sort_column', 'time');
if ($dir === '') $dir = get_pref('smd_akey_sort_dir', 'desc');
$dir = ($dir == 'asc') ? 'asc' : 'desc';
switch ($sort) {
case 'page':
$sort_sql = 'page '.$dir.', time desc';
break;
case 'triggah':
$sort_sql = 'triggah '.$dir.', time desc';
break;
case 'maximum':
$sort_sql = 'maximum '.$dir.', time desc';
break;
case 'accesses':
$sort_sql = 'accesses '.$dir.', time desc';
break;
case 'ip':
$sort_sql = 'ip '.$dir.', time desc';
break;
default:
$sort = 'time';
$sort_sql = 'time '.$dir;
break;
}
set_pref('smd_akey_sort_column', $sort, 'smd_akey', PREF_HIDDEN, '', 0, PREF_PRIVATE);
set_pref('smd_akey_sort_dir', $dir, 'smd_akey', PREF_HIDDEN, '', 0, PREF_PRIVATE);
$switch_dir = ($dir == 'desc') ? 'asc' : 'desc';
$criteria = 1;
if ($search_method and $crit) {
$crit_escaped = doSlash(str_replace(array('\\','%','_','\''), array('\\\\','\\%','\\_', '\\\''), $crit));
$critsql = array(
'page' => "page like '%$crit_escaped%'",
'triggah' => "triggah like '%$crit_escaped%'",
'maximum' => "maximum = '$crit_escaped'",
'accesses' => "accesses = '$crit_escaped'",
'ip' => "ip like '%$crit_escaped%'",
);
if (array_key_exists($search_method, $critsql)) {
$criteria = $critsql[$search_method];
$limit = 500;
} else {
$search_method = '';
$crit = '';
}
} else {
$search_method = '';
$crit = '';
}
$total = safe_count(SMD_AKEYS, "$criteria");
echo '<div id="'.$smd_akey_event.'_control" class="txp-control-panel">';
if ($total < 1) {
if ($criteria != 1) {
echo n.smd_akey_search_form($crit, $search_method).
n.graf(gTxt('no_results_found'), ' class="indicator"').'</div>';
return;
}
}
$limit = max($smd_akey_list_pageby, 15);
list($page, $offset, $numPages) = pager($total, $limit, $page);
echo n.smd_akey_search_form($crit, $search_method).'</div>';
// Retrieve the secret keyring table entries.
$secring = safe_rows('*', SMD_AKEYS, "$criteria order by $sort_sql limit $offset, $limit");
// Set up the buttons and column info.
$newbtn = '<a class="navlink" href="#" onclick="return smd_akey_togglenew();">'.gTxt('smd_akey_btn_new').'</a>';
$prefbtn = '<a class="navlink" href="?event='.$smd_akey_event.a.'step=smd_akey_prefs">'.gTxt('smd_akey_btn_pref').'</a>';
$showip = get_pref('smd_akey_log_ip', $smd_akey_prefs['smd_akey_log_ip']['default'], 1);
echo <<<EOC
<script type="text/javascript">
function smd_akey_togglenew()
{
box = jQuery("#smd_akey_create");
if (box.css("display") == "none") {
box.show();
} else {
box.hide();
}
jQuery("input.smd_focus").focus();
return false;
}
jQuery(function() {
jQuery("#smd_akey_add").click(function () {
jQuery("#smd_akey_step").val('smd_akey_create');
jQuery("#smd_akey_form").removeAttr('onsubmit').submit();
});
});
</script>
EOC;
// Inject styles.
echo '<style type="text/css">' . $smd_akey_styles['list'] . '</style>';
// Access key list.
echo n.'<div id="'.$smd_akey_event.'_container" class="txp-container txp-list">';
echo '<form name="longform" id="smd_akey_form" action="index.php" method="post" onsubmit="return verify(\''.gTxt('are_you_sure').'\')">';
echo startTable('list');
echo n.'<thead>'
.n.tr(tda($newbtn . sp . $prefbtn, ' class="noline"'))
.n.tr(
n.column_head(gTxt('smd_akey_page'), 'page', $smd_akey_event, true, $switch_dir, $crit, $search_method, (('page' == $sort) ? "$dir " : '').'page').
n.column_head(gTxt('smd_akey_trigger'), 'triggah', $smd_akey_event, true, $switch_dir, $crit, $search_method, (('triggah' == $sort) ? "$dir " : '')).
n.column_head(gTxt('smd_akey_time'), 'time', $smd_akey_event, true, $switch_dir, $crit, $search_method, (('time' == $sort) ? "$dir " : '').'date time').
n.hCell(gTxt('expires'), 'expires', ' class="date time"').
n.column_head(gTxt('smd_akey_max'), 'maximum', $smd_akey_event, true, $switch_dir, $crit, $search_method, (('maximum' == $sort) ? "$dir " : '')).
n.column_head(gTxt('smd_akey_accesses'), 'accesses', $smd_akey_event, true, $switch_dir, $crit, $search_method, (('accesses' == $sort) ? "$dir " : '')).
(($showip) ? n.column_head('IP', 'ip', $smd_akey_event, true, $switch_dir, $crit, $search_method, (('ip' == $sort) ? "$dir " : '').'ip') : '').
n.hCell('', '', ' class="multi-edit"')
).
n.'</thead>';
$multiOpts = array('smd_akey_delete' => gTxt('delete'));
echo '<tfoot>' . tr(tda(
selectInput('smd_akey_multi_edit', $multiOpts, '', true)
.n.eInput($smd_akey_event)
.n.fInput('submit', '', gTxt('go'), 'smallerbox')
,' class="multi-edit" colspan="' . (($showip) ? 7 : 6) . '" style="text-align: right; border: none;"'));
echo '</tfoot>';
echo '<tbody>';
// New access key row.
echo '<tr id="smd_akey_create" class="smd_hidden">';
echo td(fInput('hidden', 'step', 'smd_akey_multi_edit', '', '', '', '', '', 'smd_akey_step').fInput('text', 'smd_akey_newpage', '', 'smd_focus', '', '', '60'))
.td(fInput('text', 'smd_akey_triggah', ''))
.td(fInput('text', 'smd_akey_time', safe_strftime('%Y-%m-%d %H:%M:%S'), '', '', '', '25'))
.td(fInput('text', 'smd_akey_expires', '', '', '', '', '25'))
.td(fInput('text', 'smd_akey_maximum', '', '', '', '', '5'))
.td(' ')
. (($showip) ? td(' ') : '')
.td(fInput('submit', 'smd_akey_add', gTxt('add'), 'smallerbox', '', '', '', '', 'smd_akey_add'));
echo '</tr>';
// Remaining access keys.
foreach ($secring as $secidx => $data) {
if ($showip) {
$ips = do_list($data['ip'], ' ');
$iplist = array();
foreach ($ips as $ip) {
$iplist[] = ($logging == 'none') ? $ip : eLink('log', 'log_list', 'search_method', 'ip', $ip, 'crit', $ip);
}
}
$dkey = $data['page'].'|'.$data['t_hex'];
$timeparts = do_list($data['t_hex'], '-');
$expiry = (isset($timeparts[1])) ? hexdec($timeparts[1]) : '';
echo tr(
td('<a href="'.$data['page'].'">'.$data['page'].'</a>', '', 'page')
. td($data['triggah'])
. td(safe_strftime('%Y-%m-%d %H:%M:%S', $data['time']), 85, 'date time')
. td( (($expiry) ? safe_strftime('%Y-%m-%d %H:%M:%S', $expiry) : '-'), 85, 'date time')
. td($data['maximum'])
. td($data['accesses'])
. ( ($showip) ? td( trim(join(' ', $iplist)), 20, 'ip' ) : '' )
. td( fInput('checkbox', 'selected[]', $dkey, 'checkbox'), '', 'multi-edit')
);
}
echo '</tbody>';
echo endTable();
echo '</form>';
echo '<div id="'.$smd_akey_event.'_navigation" class="txp-navigation">'.
n.nav_form($smd_akey_event, $page, $numPages, $sort, $dir, $crit, $search_method, $total, $limit).
n.pageby_form($smd_akey_event, $smd_akey_list_pageby).
n.'</div>'.n.'</div>';
} else {
// Table not installed.
$btnInstall = '<form method="post" action="?event='.$smd_akey_event.a.'step=smd_akey_table_install" style="display:inline">'.fInput('submit', 'submit', gTxt('smd_akey_tbl_install_lbl'), 'smallerbox').'</form>';
$btnStyle = ' style="border:0;height:25px"';
echo startTable('list');
echo tr(tda(strong(gTxt('smd_akey_prefs_some_tbl')).br.br
.gTxt('smd_akey_prefs_some_explain').br.br
.gTxt('smd_akey_prefs_some_opts'), ' colspan="2"')
);
echo tr(tda($btnInstall, $btnStyle));
echo endTable();
}
}
/**
* Change and store qty-per-page value.
*/
function smd_akey_change_pageby()
{
event_change_pageby('smd_akey');
smd_akey();
}
/**
* The search dropdown list.
*
* @param string $crit (req) Search criteria
* @param string $method (req) Search method (field used)
* @return HTML Search form
*/
function smd_akey_search_form($crit, $method)
{
global $smd_akey_event, $smd_akey_prefs;
$doip = get_pref('smd_akey_log_ip', $smd_akey_prefs['smd_akey_log_ip']['default'], 1);
$methods = array(
'page' => gTxt('smd_akey_page'),
'triggah' => gTxt('smd_akey_trigger'),
'maximum' => gTxt('smd_akey_max'),
'accesses' => gTxt('smd_akey_accesses'),
);
if ($doip) {
$methods['ip'] = gTxt('IP');
}
return search_form($smd_akey_event, '', $crit, $methods, $method, 'page');
}
/**
* Create a key from the admin side's 'New key' button.
*/
function smd_akey_create()
{
extract(gpsa(array('smd_akey_newpage', 'smd_akey_triggah', 'smd_akey_time', 'smd_akey_expires', 'smd_akey_maximum')));
if ($smd_akey_newpage) {
// Just call the public tag with the relevant options.
$key = smd_access_key(
array(
'url' => $smd_akey_newpage,
'trigger' => $smd_akey_triggah,
'start' => $smd_akey_time,
'expires' => $smd_akey_expires,
'max' => $smd_akey_maximum,
)
);
$msg = gTxt('smd_akey_generated', array('{key}' => $key));
} else {
$msg = array(gTxt('smd_akey_need_page'), E_ERROR);
}
smd_akey($msg);
}
/**
* Handle submission of the multi-edit dropdown options.
*/
function smd_akey_multi_edit()
{
$selected = gps('selected');
$operation = gps('smd_akey_multi_edit');
$del = 0;
$msg = '';
switch ($operation) {
case 'smd_akey_delete':
if ($selected) {
foreach ($selected as $sel) {
$parts = explode('|', $sel);
$ret = safe_delete(SMD_AKEYS, "page = '" . $parts[0] . "' AND t_hex = '" . $parts[1] . "'");
$del = ($ret) ? $del+1 : $del;
}
$msg = gTxt('smd_akey_deleted', array('{deleted}' => $del));
}
break;
}
smd_akey($msg);
}
/**
* Display the prefs.
*
* @return HTML Page sub-content.
*/
function smd_akey_prefs()
{
global $smd_akey_event, $smd_akey_prefs;
pagetop(gTxt('smd_akey_pref_legend'));
$out = array();
$out[] = '<form name="smd_akey_prefs" id="smd_akey_prefs" action="index.php" method="post">';
$out[] = eInput($smd_akey_event).sInput('smd_akey_prefsave');
$out[] = startTable('list');
$out[] = tr(tdcs(strong(gTxt('smd_akey_pref_legend')), 2));
foreach ($smd_akey_prefs as $idx => $prefobj) {
$subout = array();
$subout[] = tda('<label for="'.$idx.'">'.gTxt($idx).'</label>', ' class="noline" style="text-align: right; vertical-align: middle;"');
$val = get_pref($idx, $prefobj['default']);
switch ($prefobj['html']) {
case 'text_input':
$subout[] = fInputCell($idx, $val, '', '', '', $idx);
break;
case 'yesnoradio':
$subout[] = tda(yesnoRadio($idx, $val),' class="noline"');
break;
}
$out[] = tr(join(n, $subout));
}
$out[] = tr(tda(' ', ' class="noline"') . tda(fInput('submit', '', gTxt('save'), 'publish'), ' class="noline"'));
$out[] = endTable();
$out[] = '</form>';
echo join(n, $out);
}
/**
* Save the prefs.
*/
function smd_akey_prefsave()
{
global $smd_akey_event, $smd_akey_prefs;
foreach ($smd_akey_prefs as $idx => $prefobj) {
$val = ps($idx);
set_pref($idx, $val, $smd_akey_event, $prefobj['type'], $prefobj['html'], $prefobj['position']);
}
$msg = gTxt('smd_akey_prefs_saved');
smd_akey($msg);
}
/**
* Add the smd_akeys table if not already installed.
*
* @param bool $showpane (opt) Whether to operate silently or display the admin interface on completion
*/
function smd_akey_table_install($showpane = '1')
{
global $DB;
$GLOBALS['txp_err_count'] = 0;
$ret = '';
$sql = array();
// Use 'triggah' and 'maximum' because 'trigger' and 'max' are reserved words.
$sql[] = "CREATE TABLE IF NOT EXISTS `" . PFX . SMD_AKEYS . "` (
`page` varchar(255) default '',
`t_hex` varchar(17) default '',
`time` int(14) default 0,
`secret` varchar(511) default '',
`triggah` varchar(255) default '',
`maximum` int(11) default 0,
`accesses` int(11) default 0,
`ip` text,
PRIMARY KEY (`page`,`t_hex`)
) ENGINE=MyISAM";
if (gps('debug')) {
dmp($sql);
}
foreach ($sql as $qry) {
$ret = safe_query($qry);
if ($ret===false) {
$GLOBALS['txp_err_count']++;
echo "<b>".$GLOBALS['txp_err_count'].".</b> " . mysqli_error($DB->link) . "<br />\n";
echo "<!--\n $qry \n-->\n";
}
}
// Spit out results.
if ($GLOBALS['txp_err_count'] == 0) {
if ($showpane) {
$msg = gTxt('smd_akey_tbl_installed');
smd_akey($msg);
}
} else {
if ($showpane) {
$msg = gTxt('smd_akey_tbl_not_installed');
smd_akey($msg);
}
}
}
/**
* Drop smd_akeys table if in database.
*/
function smd_akey_table_remove()
{
global $DB;
$ret = '';
$sql = array();
$GLOBALS['txp_err_count'] = 0;
if (smd_akey_table_exist()) {
$sql[] = "DROP TABLE IF EXISTS " .PFX . SMD_AKEYS. "; ";
if (gps('debug')) {
dmp($sql);
}
foreach ($sql as $qry) {
$ret = safe_query($qry);
if ($ret===false) {
$GLOBALS['txp_err_count']++;
echo "<b>".$GLOBALS['txp_err_count'].".</b> " . mysqli_error($DB->link) . "<br />\n";
echo "<!--\n $qry \n-->\n";
}
}
}
if ($GLOBALS['txp_err_count'] == 0) {
$msg = gTxt('smd_akey_tbl_removed');
} else {
$msg = gTxt('smd_akey_tbl_not_removed');
smd_akey($msg);
}
}
/**
* Check if the smd_akeys table exists.
*
* @param bool $type (opt) Full check (1), or column count check
* @return bool | column count
*/
function smd_akey_table_exist($type = '')
{
global $plugins_ver, $DB;
// Upgrade check.
$ver = get_pref('smd_akey_installed_version', '');
if (!$ver || $plugins_ver['smd_access_keys'] != $ver) {
// Increase the size of the t_hex field to allow for expiry times.
$ret = @safe_field("CHARACTER_MAXIMUM_LENGTH", "INFORMATION_SCHEMA.COLUMNS", "table_name = '" . PFX . SMD_AKEYS . "' AND table_schema = '" . $DB->db . "' AND column_name = 't_hex'");
if ($ret != '17') {
safe_alter(SMD_AKEYS, "CHANGE `t_hex` `t_hex` VARCHAR( 17 ) DEFAULT ''");
}
// Increase the size of the secret field to allow for longer keys.
$ret = @safe_field("CHARACTER_MAXIMUM_LENGTH", "INFORMATION_SCHEMA.COLUMNS", "table_name = '" . PFX . SMD_AKEYS . "' AND table_schema = '" . $DB->db . "' AND column_name = 'secret'");
if ($ret != '511') {
safe_alter(SMD_AKEYS, "CHANGE `secret` `secret` VARCHAR( 511 ) DEFAULT ''");
}
set_pref('smd_akey_installed_version', $plugins_ver['smd_access_keys'], 'smd_akey', PREF_HIDDEN, '', 0);
}
if ($type == '1') {
$tbls = array(SMD_AKEYS => 8);
$out = count($tbls);
foreach ($tbls as $tbl => $cols) {
if (count(@safe_show('columns', $tbl)) == $cols) {
$out--;
}
}
return ($out===0) ? 1 : 0;
} else {
return(@safe_show('columns', SMD_AKEYS));
}
}
/**
* Callback, issued just before a download is initiated.
*
* @param string $evt (req) Textpattern event
* @param string $stp (req) Textpattern step
*/
function smd_access_protect_download($evt, $stp)
{
global $smd_akey_prefs, $id, $file_error;
if (smd_akey_table_exist(1) && !isset($file_error)) {
$fileid = intval($id);
// In case the page was called with a bogus filename, get the "true" filename
// from the database and make up the valid URL.
$real_file = safe_field("filename", "txp_file", "id=".doSlash($fileid));
$page = filedownloadurl($fileid, $real_file);
$secring = safe_field('page', SMD_AKEYS, "page='".doSlash($page)."'");
// Only want to protect pages that we've generated tokens for.
if ($secring) {
// Pass in a default expiry from the pref, but it can be overridden by the key's expiry.
return smd_access_protect(
array(
'trigger' => 'file_download',
'force' => '1',
'expires' => get_pref('smd_akey_file_download_expires', $smd_akey_prefs['smd_akey_file_download_expires']['default']),
)
);
}
}
// Remote download not done - leave to Txp to handle error or "local" file download.
return;
}
/**
* PUBLIC SIDE INTERFACE
* =====================
*
* Tag: Generate an access token.
*
* @param array $atts (req) Tag attribute list
* @param array $thing (opt) Tag container content
* @return HTML
*/
function smd_access_key($atts, $thing = null)
{
global $smd_akey_prefs, $smd_akey_info;
// In case this tag is called from the admin side - needs parse().
include_once txpath.'/publish.php';
extract(lAtts(array(
'secret' => '',
'url' => '',
'site_name' => '1',
'section_mode' => '0',
'start' => '',
'expires' => '',
'trigger' => 'smd_akey',
'max' => '',
'extra' => '',
'form' => '',
'strength' => 'SMD_SSL_RAND', // or SMD_MD5, or your own salt
), $atts));
if (smd_akey_table_exist(1)) {
$thing = (empty($form)) ? $thing : fetch_form($form);
$thing = (empty($thing)) ? '<txp:smd_access_info item="key" />' : $thing;
$trigger = trim($trigger);
$trigger = ($trigger == 'file_download') ? '' : $trigger;
$hasSSL = function_exists('openssl_random_pseudo_bytes');
$smd_akey_salt_length = get_pref('smd_akey_salt_length', $smd_akey_prefs['smd_akey_salt_length']['default']);
// Without a URL, assume current page.
$page = rtrim( (($url) ? $url : serverSet('REQUEST_URI')), '/');
if ($site_name && (strpos($page, 'http') !== 0)) {
// Can't use raw hu since it contains the subdir (as does the REQUEST_URI)
// so duplicate portions would occur in the generated URL.
$urlparts = parse_url(hu);
$page = $urlparts['scheme'] . '://' . $urlparts['host'] . $page;
}
if (!$secret) {
if ($hasSSL) {
$secret = bin2hex(openssl_random_pseudo_bytes($smd_akey_salt_length * 8));
} else {
$secret = uniqid('', true);
}
}
if ($strength === 'SMD_SSL_RAND' && $hasSSL) {
// Need to divide by 2 to retain the same key length as v0.1x, because each byte
// here produces two hex characters.
$salt = bin2hex(openssl_random_pseudo_bytes(floor($smd_akey_salt_length / 2)));
} elseif ($strength === 'SMD_MD5') {
$salt = substr(md5(uniqid(mt_rand(), true)), 0, $smd_akey_salt_length);
} else {
$salt = substr($strength, 0, $smd_akey_salt_length);
}
$plen = strlen($page) % 32; // Because 32 is the size of an md5 string and we don't want to fall off the end.
// Generate a timestamp. The clock starts ticking from this moment.
$ts = ($start) ? safe_strtotime($start) : time();
$ts = ($ts === false) ? time() : $ts;
$t_hex = dechex($ts);
// Any expiry to add?
if ($expires) {
// Relative offset from the start time, or an absolute expiry?
$rel = (strpos($expires, '+') === 0) ? true : false;
if ($rel) {
$exp = safe_strtotime($expires, $ts);
} else {
$exp = safe_strtotime($expires);
}
if ($exp !== false) {
$t_hex .= '-' . dechex($exp);
}
}
// Update/insert the remaining data.
$exists = safe_field('page', SMD_AKEYS, "page='".doSlash($page)."' AND t_hex='".doSlash($t_hex)."'");
$maxinfo = '';
if ($max) {
$maxinfo = ", maximum = '".doSlash($max)."', accesses = '0'";
}
if ($exists) {
safe_update(SMD_AKEYS, "triggah='".doSlash($trigger)."', time='".doSlash($ts)."', secret='".doSlash($secret)."'" . $maxinfo, "page='".doSlash($page)."' AND t_hex='".doSlash($t_hex)."'");
} else {
safe_insert(SMD_AKEYS, "page='".doSlash($page)."', t_hex='".doSlash($t_hex)."', triggah='".doSlash($trigger)."', secret='".doSlash($secret)."', time='".doSlash($ts)."'" . $maxinfo);
}
// Tack on max if applicable.
$max_safe = $max;
$max = ($max) ? '.'.$max : '';
// And any extra.
$extratok = ($extra) ? '/'.$extra : '';
// Create the raw token...
$token = md5($salt.$secret.$page.$trigger.$t_hex.$max.$extra);
// ... and insert the salt partway through.
$salty_token = substr($token, 0, $plen) . $salt . substr($token, $plen);
$tokensep = ($section_mode) ? '?' : '/';
$key = $page . (($trigger) ? $tokensep . $trigger : '') . '/' . $salty_token . '/' . $t_hex . $max . $extratok;
$smd_akey_info = array(
'ak_page' => $page,
'ak_extra' => $extra,
'ak_hextime' => $t_hex,
'ak_issued' => $ts,
'ak_now' => time(),
'ak_expires' => ($expires) ? $exp : '',
'ak_trigger' => $trigger,
'ak_maximum' => $max_safe,
'ak_separator' => $tokensep,
'ak_key' => $key,
);
return parse($thing);
} else {
trigger_error(gTxt('smd_akey_tbl_not_installed'), E_USER_NOTICE);
}
}
/**
* Tag: Protect a page resource.
*
* The resource is protected for a given time limit from the moment the
* access token has been generated.
*
* Embed this tag at the top of the page to protect, or wrap it around
* part of a page to protect. The unique URL to unlock the resource
* is generated by <txp:smd_access_key />.
*
* @param array $atts (req) Tag attribute list
* @param array $thing (opt) Tag container content
* @return HTML Protected resource | error
*/
function smd_access_protect($atts, $thing = null)
{
global $smd_access_error, $smd_access_errcode, $smd_akey_protected_info, $smd_akey_prefs, $permlink_mode, $plugins;
extract(lAtts(array(
'trigger' => 'smd_akey',
'trigger_mode' => 'exact', // exact, begins, ends, contains
'site_name' => '1',
'section_mode' => '0',
'force' => '0',
'expires' => '3600', // in seconds
), $atts));
if (smd_akey_table_exist(1)) {
$url = serverSet('REQUEST_URI');
if ($site_name && (strpos($url, hu) === false)) {
$urlparts = parse_url(hu);
// Can't use raw hu since it contains the subdir (as does the REQUEST_URI)
// so duplicates would occur in the generated URL.
$url = $urlparts['scheme'] . '://' . $urlparts['host'] . $url;
}
if ($section_mode == '1') {
$halves = explode('?', $url);
$half1 = explode('/', $halves[0]);
$half2 = (isset($halves[1])) ? explode('/', $halves[1]) : array();
$parts = array_merge($half1, $half2);
} else {
$parts = explode('/', $url);
}
trace_add('[smd_access_key URL elements: ' . join('|', $parts).']');
// Look for one of the triggers in the URL and bomb out if we find it.
$triggers = do_list($trigger);
$trigger = $triggers[0]; // Initialise to the first value in case no others are found.
$trigoff = false;
foreach ($triggers as $trig) {
switch ($trigger_mode) {
case 'exact':
$trigoff = array_search($trig, $parts);
$realTrig = $trig;
break;
case 'begins':
$count = 0;
foreach ($parts as $part) {
if (strpos($part, $trig) === 0) {
$trigoff = $count;
$realTrig = $part;
break;
}
$count++;
}
break;
case 'ends':
$count = 0;
foreach ($parts as $part) {
$re = '/.+'.preg_quote($trig).'$/i';
if (preg_match($re, $part) === 1) {
$trigoff = $count;
$realTrig = $part;
break;
}
$count++;
}
break;
case 'contains':
$count = 0;
foreach ($parts as $part) {
$re = '/.*'.preg_quote($trig).'.*$/i';
if (preg_match($re, $part) === 1) {
$trigoff = $count;
$realTrig = $part;
break;
}
$count++;
}
break;
}
if ($trigoff !== false) {
// Found it so set the trigger to be the current item and jump out.
$trigoff = ($trigger == 'file_download') ? $trigoff + 2 : $trigoff;
$trigger = $realTrig;
break;
}
}
trace_add('[smd_access_key trigger: ' . $trigger . ($trigoff ? ' found at ' . $trigoff : '') . ']');
$ret = false;
$smd_access_error = $smd_access_errcode = '';
$smd_akey_salt_length = get_pref('smd_akey_salt_length', $smd_akey_prefs['smd_akey_salt_length']['default']);
$doip = get_pref('smd_akey_log_ip', $smd_akey_prefs['smd_akey_log_ip']['default']);
if ($trigoff !== false) {
$tokidx = $trigoff + 1;
$timeidx = $trigoff + 2;
$extraidx = $trigoff + 3;
// OK, on a trigger page, so read the token from the URL.
$tok = (isset($parts[$tokidx]) && strlen($parts[$tokidx]) == intval(32 + $smd_akey_salt_length)) ? $parts[$tokidx] : 0;
trace_add('[smd_access_key token: ' . $tok .']');
if ($tok) {
// The token is present, so read the timestamp from the URL.
$t_hex = (isset($parts[$timeidx])) ? $parts[$timeidx] : 0;
// Is there a download limit? Extract it if so.
$timeparts = do_list($t_hex, '.');
$max = (isset($timeparts[1])) ? $timeparts[1] : '0';
$maxtok = ($max) ? '.'.$max : '';
$t_hex = $timeparts[0];
// Any extra info?
$extras = (isset($parts[$extraidx])) ? array_slice($parts, $extraidx) : array();
// Recreate the original page URL, sans /trigger/token/time.
if ($trigger == 'file_download') {
$trigoff++;
$trigger = '';
}
// gbp_permanent_links sets messy mode behind the scenes but still uses non-messy URLs
// so it requires an exception.
$gbp_pl = (is_array($plugins) && in_array('gbp_permanent_links', $plugins));
if ($permlink_mode == 'messy' && !$gbp_pl) {
// Don't want a slash between site and start of query params.
$page = rtrim(join('/', array_slice($parts, 0, $trigoff-1)), '/') . $parts[$trigoff-1];
} else {
$page = rtrim(join('/', array_slice($parts, 0, $trigoff)), '/');
}
// In case the URL contains non-ascii chars.
$page = rawurldecode($page);
trace_add('[smd_access_key page | timestamp | max | extras: ' . join('|', array($page, $t_hex, $max, $extras)) . ']');
if ($t_hex) {
// The timestamp is present. Next, get the secret key.
$secret = false;
$secring = safe_row('*', SMD_AKEYS, "page='".doSlash($page)."' AND t_hex = '".doSlash($t_hex)."'");
if ($secring) {
$secret = $secring['secret'];
// Extract the salt from the token.
$plen = strlen($page) % 32;
$salt = substr($tok, $plen, $smd_akey_salt_length);
$tok = substr($tok, 0, $plen).substr($tok, $plen+$smd_akey_salt_length);
$ext = (($extras) ? urldecode(join('/', $extras)) : '');
// Regenerate the original token...
$check_token = md5($salt.$secret.$page.$trigger.$t_hex.$maxtok.$ext);
trace_add('[smd_access_key reconstructed token: ' . $check_token . ']');
// ... and compare it to the one in the URL.
if ($check_token == $tok) {
// Token is valid. Now check if the page has expired.
// Is there an explicit access key expiry? Extract that if so.
$timeparts = do_list($t_hex, '-');
$t_exp = (isset($timeparts[1])) ? hexdec($timeparts[1]) : '';
$t_beg = $timeparts[0];
$t_dec = hexdec($t_beg);
$now = time();
// Has the resource become available yet?
if ($now < $t_dec) {
if ($thing == null) {
txp_die(gTxt('smd_akey_err_unavailable'), 410);
} else {
$smd_access_error = 'smd_akey_err_unavailable';
$smd_access_errcode = 410;
}
} else {
// Has token's expiry been reached, or is 'now' greater than 'then' (when token generated) + expiry period?
if ($t_exp) {
$tester = true;
$compare_to = $t_exp;
} else {
$tester = ($expires != 0);
$compare_to = $t_dec + $expires;
}
if ($tester && ($now > $compare_to)) {
if ($thing == null) {
txp_die(gTxt('smd_akey_err_expired'), 410);
} else {
$smd_access_error = 'smd_akey_err_expired';
$smd_access_errcode = 410;
}
} else {
// Check if the download limit has been exceeded.
$vu_qty = $secring['accesses'];
if ($max) {
if ($vu_qty < $max) {
$ret = true;
} else {
if ($thing == null) {
txp_die(gTxt('smd_akey_err_limit'), 410);
} else {
$smd_access_error = 'smd_akey_err_limit';
$smd_access_errcode = 410;
}
}
} else {
$ret = true;
}
// Increment the access counter.
$vu_qty++;
// Grab the IP and add it to the list of IPs so far.
if ($doip) {
$ips = do_list($secring['ip'], ' ');
$ip = remote_addr();
if (!in_array($ip, $ips)) {
$ips[] = $ip;
}
$ipup = ", ip='".doSlash(trim(join(' ', $ips)))."'";
} else {
$ipup = '';
}
safe_update(SMD_AKEYS, "accesses='".doSlash($vu_qty)."'" . $ipup, "page='".doSlash($page)."' AND t_hex = '".doSlash($t_hex)."'");
// Load up the global array so <txp:smd_access_info> and <txp:if_smd_access_info> work.
$smd_akey_protected_info = array(
'page' => $secring['page'],
'hextime' => $secring['t_hex'],
'issued' => $secring['time'],
'now' => $now,
'expires' => $compare_to,
'trigger' => $secring['triggah'],
'maximum' => $secring['maximum'],
'accesses' => $vu_qty,
);
if ($doip) {
$smd_akey_protected_info['ip'] = $ip;
}
if ($extras) {
$smd_akey_protected_info['extra'] = urldecode(join('/', $extras));
foreach ($extras as $idx => $extra) {
$smd_akey_protected_info['extra_'.intval($idx+1)] = urldecode($extra);
}
}
}
}
} else {
if ($thing == null) {
txp_die(gTxt('smd_akey_err_invalid_token'), 403);
} else {
$smd_access_error = 'smd_akey_err_invalid_token';
$smd_access_errcode = 403;
}
}
} else {
if ($thing == null) {
txp_die(gTxt('smd_akey_err_unauthorized'), 401);
} else {
$smd_access_error = 'smd_akey_err_unauthorized';
$smd_access_errcode = 401;
}
}
} else {
if ($thing == null) {
txp_die(gTxt('smd_akey_err_missing_timestamp'), 403);
} else {
$smd_access_error = 'smd_akey_err_missing_timestamp';
$smd_access_errcode = 403;
}
}
} else {
if ($thing == null) {
txp_die(gTxt('smd_akey_err_bad_token'), 403);
} else {
$smd_access_error = 'smd_akey_err_bad_token';
$smd_access_errcode = 403;
}
}
} else {
// If we always want to forbid access to this page regardless if the trigger exists.
if ($force) {
if ($thing == null) {
txp_die(gTxt('smd_akey_err_forbidden'), 401);
} else {
$smd_access_error = 'smd_akey_err_forbidden';
$smd_access_errcode = 401;
}
} else {
$ret = true;
}
}
if ($smd_access_error || $smd_access_errcode) {
trace_add('[smd_access_key error state: ' . $smd_access_errcode . '|' . $smd_access_error . ']');
}
// If we reach this point it's because we're using a container.
return parse(EvalElse($thing, $ret));
} else {
trigger_error(gTxt('smd_akey_tbl_not_installed'), E_USER_NOTICE);
}
}
/**
* Conditional tag for checking error status from smd_access_protect.
*
* @param array $atts (req) Tag attribute list
* @param array $thing (opt) Tag container content
* @return HTML
*/
function smd_if_access_error($atts, $thing = null)
{
global $smd_access_error, $smd_access_errcode;
extract(lAtts(array(
'type' => '',
'code' => '',
), $atts));
$err = array();
$codes = do_list($code);
$types = do_list($type);
if ($smd_access_error) {
if ($code && $type) {
$err['code'] = (in_array($smd_access_errcode, $codes)) ? true : false;
$err['msg'] = (in_array($smd_access_error, $types)) ? true : false;
} elseif ($code) {
$err['code'] = (in_array($smd_access_errcode, $codes)) ? true : false;
} elseif ($type) {
$err['msg'] = (in_array($smd_access_error, $types)) ? true : false;
} else {
$err['msg'] = true;
}
}
$out = in_array(false, $err) ? false : true; // AND logic
return parse(EvalElse($thing, $out));
}
/**
* Display access error information.
*
* @param array $atts (req) Tag attribute list
* @param array $thing (opt) Tag container content
* @return HTML
*/
function smd_access_error($atts, $thing = null)
{
global $smd_access_error, $smd_access_errcode;
extract(lAtts(array(
'item' => 'message',
'message' => '',
'wraptag' => '',
'class' => '',
'html_id' => '',
'break' => '',
'breakclass' => '',
), $atts));
$out = array();
$items = do_list($item);
if ($smd_access_errcode && in_array('code', $items)) {
$out[] = $smd_access_errcode;
}
if ($smd_access_error && in_array('message', $items)) {
$out[] = ($message) ? $message : gTxt($smd_access_error);
}
if ($out) {
return doWrap($out, $wraptag, $break, $class, $breakclass, '', '', $html_id);
}
return '';
}
/**
* Display pieces of access information, for custom formatted messages.
*
* @param array $atts (req) Tag attribute list
* @param array $thing (opt) Tag container content
* @return HTML
*/
function smd_access_info($atts, $thing = null)
{
global $smd_akey_protected_info, $smd_akey_info;
extract(lAtts(array(
'item' => 'page',
'escape' => 'html',
'format' => '%Y-%m-%d %H:%M:%S',
'wraptag' => '',
'class' => '',
'html_id' => '',
'break' => '',
'breakclass' => '',
), $atts));
$out = array();
$items = do_list($item);
foreach ($items as $idx) {
$ak_idx = 'ak_'.$idx;
if ($smd_akey_protected_info && array_key_exists($idx, $smd_akey_protected_info)) {
$val = ($escape == 'html') ? htmlspecialchars($smd_akey_protected_info[$idx]) : $smd_akey_protected_info[$idx];
if (in_array($idx, array('time', 'now', 'expires')) && $format) {
$val = safe_strftime($format, $val);
}
$out[] = $val;
}
if ($smd_akey_info && array_key_exists($ak_idx, $smd_akey_info)) {
$val = ($escape == 'html') ? htmlspecialchars($smd_akey_info[$ak_idx]) : $smd_akey_info[$ak_idx];
if (in_array($idx, array('time', 'now', 'expires')) && $format) {
$val = safe_strftime($format, $val);
}
$out[] = $val;
}
}
if ($out) {
return doWrap($out, $wraptag, $break, $class, $breakclass, '', '', $html_id);
}
return '';
}