Files
aowow/pages/guide.php
Sarjuuk 682b315e17 User/Misc
* floating changes
 * codify user checks into functions
2025-04-02 23:26:01 +02:00

550 lines
25 KiB
PHP

<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// menuId ?: Category g_initPath()
// tabid 6: Guides g_initHeader()
class GuidePage extends GenericPage
{
use TrDetailPage;
const SHOW_NEW = 1;
const SHOW_EDITOR = 2;
const SHOW_GUIDE = 3;
const SHOW_CHANGELOG = 4;
const VALID_URL = '/^[a-z0-9=_&\.\/\-]{2,64}$/i';
protected /* array */ $guideRating = [];
protected /* ?string */$extraHTML = null;
protected /* int */ $type = Type::GUIDE;
protected /* int */ $typeId = 0;
protected /* int */ $guideRevision = -1;
protected /* string */ $tpl = 'detail-page-generic';
protected /* array */ $path = [6];
protected /* int */ $tabId = 6;
protected /* int */ $mode = CACHE_TYPE_PAGE;
protected /* string */ $author = '';
protected /* array */ $gPageInfo = [];
protected /* int */ $show = self::SHOW_GUIDE;
private /* array */ $validCats = [1, 2, 3, 4, 5, 6, 7, 8, 9];
private /* string */ $extra = '';
private /* string */ $powerTpl = '$WowheadPower.registerGuide(%s, %d, %s);';
private /* array */ $editorFields = [];
private /* bool */ $save = false;
protected /* array */ $_get = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'],
'rev' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt']
);
protected /* array */ $_post = array(
'save' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet'],
'submit' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet'],
'title' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'],
'name' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'],
'description' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GuidePage::checkDescription'],
'changelog' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextBlob'],
'body' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextBlob'],
'locale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'],
'category' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'],
'specId' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'],
'classId' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt']
);
public function __construct($pageCall, $pageParam)
{
$guide = explode( "&", $pageParam, 2);
parent::__construct($pageCall, $pageParam);
if (isset($guide[1]) && preg_match(self::VALID_URL, $guide[1]))
$this->extra = $guide[1];
/**********************/
/* get mode + guideId */
/**********************/
if (Util::checkNumeric($guide[0], NUM_CAST_INT))
$this->typeId = $guide[0];
else if (preg_match(self::VALID_URL, $guide[0]))
{
switch ($guide[0])
{
case 'changelog':
if (!$this->_get['id'])
break;
$this->show = self::SHOW_CHANGELOG;
$this->tpl = 'text-page-generic';
$this->article = false; // do not include article from db
// main container should be tagged: <div class="text guide-changelog">
// why is this here: is there a mediawiki like diff function for staff?
$this->addScript([SC_CSS_STRING, 'li input[type="radio"] {margin:0}']);
$this->typeId = $this->_get['id']; // just to display sensible not-found msg
if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `id` = ?d', $this->typeId))
$this->typeId = intVal($id);
break;
case 'new':
if (User::canWriteGuide())
{
$this->show = self::SHOW_NEW;
$this->guideRevision = null;
$this->initNew();
return; // do not create new GuideList
}
break;
case 'edit':
if (User::canWriteGuide())
{
if (!$this->initEdit())
$this->notFound(Lang::game('guide'), Lang::guide('notFound'));
$this->show = self::SHOW_EDITOR;
}
break;
default:
if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `url` = ?', Util::lower($guide[0])))
{
$this->typeId = intVal($id);
$this->guideRevision = null;
$this->articleUrl = Util::lower($guide[0]);
}
}
}
/*********************/
/* load actual guide */
/*********************/
$this->subject = new GuideList(array(['id', $this->typeId]));
if ($this->subject->error)
$this->notFound(Lang::game('guide'), Lang::guide('notFound'));
if (!$this->subject->canBeViewed() && !$this->subject->userCanView())
header('Location: ?guides='.$this->subject->getField('category'), true, 302);
if ($this->show == self::SHOW_GUIDE && $this->_get['rev'] !== null && !$this->articleUrl && $this->subject->userCanView())
$this->guideRevision = $this->_get['rev'];
else if ($this->show == self::SHOW_GUIDE && !$this->articleUrl)
$this->guideRevision = $this->subject->getField('rev');
else
$this->guideRevision = null;
if (!$this->name)
$this->name = $this->subject->getField('name');
}
protected function generateContent() : void
{
match ($this->show)
{
self::SHOW_NEW => $this->displayNew(),
self::SHOW_EDITOR => $this->displayEditor(),
self::SHOW_GUIDE => $this->displayGuide(),
self::SHOW_CHANGELOG => $this->displayChangelog(),
default => trigger_error('GuidePage::generateContent - what content!?')
};
}
private function displayNew() : void
{
// init required template vars
$this->editorFields = array(
'locale' => Lang::getLocale()->value,
'status' => GUIDE_STATUS_DRAFT
);
}
private function displayEditor() : void
{
// can't check in init as subject is unknown
if ($this->subject->getField('status') == GUIDE_STATUS_ARCHIVED)
$this->notFound(Lang::game('guide'), Lang::guide('notFound'));
$status = GUIDE_STATUS_NONE;
$rev = DB::Aowow()->selectCell('SELECT `rev` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d ORDER BY `rev` DESC LIMIT 1', Type::GUIDE, $this->typeId);
$curStatus = DB::Aowow()->selectCell('SELECT `status` FROM ?_guides WHERE `id` = ?d ', $this->typeId);
if ($rev === null)
$rev = 0;
if ($this->save)
{
$rev++;
// insert Article
DB::Aowow()->query('INSERT INTO ?_articles (`type`, `typeId`, `locale`, `rev`, `editAccess`, `article`) VALUES (?d, ?d, ?d, ?d, ?d, ?)',
Type::GUIDE, $this->typeId, $this->_post['locale'], $rev, User::$groups & U_GROUP_STAFF ? User::$groups : User::$groups | U_GROUP_BLOGGER, $this->_post['body']);
// link to Guide
$guideData = array(
'category' => $this->_post['category'],
'classId' => $this->_post['classId'],
'specId' => $this->_post['specId'],
'title' => $this->_post['title'],
'name' => $this->_post['name'],
'description' => $this->_post['description'] ?: Lang::trimTextClean((new Markup($this->_post['body']))->stripTags(), 120),
'locale' => $this->_post['locale'],
'roles' => User::$groups,
'status' => GUIDE_STATUS_DRAFT
);
DB::Aowow()->query('UPDATE ?_guides SET ?a WHERE `id` = ?d', $guideData, $this->typeId);
// new guide -> reload editor
if ($this->_get['id'] === 0)
header('Location: ?guide=edit&id='.$this->typeId, true, 302);
else
DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `rev`, `date`, `userId`, `msg`) VALUES (?d, ?d, ?d, ?d, ?)', $this->typeId, $rev, time(), User::$id, $this->_post['changelog']);
if ($this->_post['submit'])
{
$status = GUIDE_STATUS_REVIEW;
if ($curStatus != GUIDE_STATUS_REVIEW)
{
DB::Aowow()->query('UPDATE ?_guides SET `status` = ?d WHERE `id` = ?d', GUIDE_STATUS_REVIEW, $this->typeId);
DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `status`) VALUES (?d, ?d, ?d, ?d)', $this->typeId, time(), User::$id, GUIDE_STATUS_REVIEW);
}
}
}
// init required template vars
$this->editorFields = array(
'category' => $this->_post['category'] ?? $this->subject->getField('category'),
'title' => $this->_post['title'] ?? $this->subject->getField('title'),
'name' => $this->_post['name'] ?? $this->subject->getField('name'),
'description' => $this->_post['description'] ?? $this->subject->getField('description'),
'text' => $this->_post['body'] ?? $this->subject->getArticle(),
'status' => $status ?: $this->subject->getField('status'),
'classId' => $this->_post['classId'] ?? $this->subject->getField('classId'),
'specId' => $this->_post['specId'] ?? $this->subject->getField('specId'),
'locale' => $this->_post['locale'] ?? $this->subject->getField('locale'),
'rev' => $rev
);
$this->extendGlobalData($this->subject->getJSGlobals());
}
private function displayGuide() : void
{
if (!($this->subject->getField('cuFlags') & GUIDE_CU_NO_QUICKFACTS))
{
$qf = [];
if ($this->subject->getField('cuFlags') & CC_FLAG_STICKY)
$qf[] = '[span class=guide-sticky]'.Lang::guide('sticky').'[/span]';
$qf[] = Lang::guide('author').Lang::main('colon').'[url=?user='.$this->subject->getField('author').']'.$this->subject->getField('author').'[/url]';
if ($this->subject->getField('category') == 1)
{
$c = $this->subject->getField('classId');
$s = $this->subject->getField('specId');
if ($c > 0)
{
$this->extendGlobalIds(Type::CHR_CLASS, $c);
$qf[] = Util::ucFirst(Lang::game('class')).Lang::main('colon').'[class='.$c.']';
}
if ($s > -1)
$qf[] = Lang::guide('spec').Lang::main('colon').'[icon class="c'.$c.' icontiny" name='.Game::$specIconStrings[$c][$s].']'.Lang::game('classSpecs', $c, $s).'[/icon]';
}
// $qf[] = Lang::guide('patch').Lang::main('colon').'3.3.5'; // replace with date
$qf[] = Lang::guide('added').Lang::main('colon').'[tooltip name=added]'.date('l, G:i:s', $this->subject->getField('date')).'[/tooltip][span class=tip tooltip=added]'.date(Lang::main('dateFmtShort'), $this->subject->getField('date')).'[/span]';
switch ($this->subject->getField('status'))
{
case GUIDE_STATUS_APPROVED:
$qf[] = Lang::guide('views').Lang::main('colon').'[n5='.$this->subject->getField('views').']';
if (!($this->subject->getField('cuFlags') & GUIDE_CU_NO_RATING))
{
$this->guideRating = array(
$this->subject->getField('rating'), // avg rating
User::canUpvote() && User::canDownvote() ? 'true' : 'false',
$this->subject->getField('_self'), // my rating amt; 0 = no vote
$this->typeId // guide Id
);
if ($this->subject->getField('nvotes') < 5)
$qf[] = Lang::guide('rating').Lang::main('colon').Lang::guide('noVotes');
else
$qf[] = Lang::guide('rating').Lang::main('colon').Lang::guide('votes', [round($this->subject->getField('rating'), 1), $this->subject->getField('nvotes')]);
}
break;
case GUIDE_STATUS_ARCHIVED:
$qf[] = Lang::guide('status', GUIDE_STATUS_ARCHIVED);
break;
}
$qf = '[ul][li]'.implode('[/li][li]', $qf).'[/li][/ul]';
if ($this->subject->getField('status') == GUIDE_STATUS_REVIEW && User::isInGroup(U_GROUP_STAFF) && $this->_get['rev'])
{
$this->addScript([SC_JS_STRING, '
DomContentLoaded.addEvent(function() {
let send = function (status)
{
let message = "";
let id = $WH.g_getGets().guide;
if (status == 4) // rejected
{
while (message === "")
message = prompt("Please provide your reasoning.");
if (message === null)
return false;
}
$.ajax({cache: false, url: "?admin=guide", type: "POST",
error: function() {
alert("Operation failed.");
},
success: function(json) {
if (json != 1)
alert("Operation failed.");
else
window.location.href = "?admin=guides";
},
data: { id: id, status: status, msg: message }
})
return true;
};
$WH.ge("btn-accept").onclick = send.bind(null, 3);
$WH.ge("btn-reject").onclick = send.bind(null, 4);
});
']);
$qf .= '[h3 style="text-align:center"]Admin[/h3]';
$qf .= '[div style="text-align:center"][url=# id="btn-accept" class=icon-tick]Approve[/url][url=# style="margin-left:20px" id="btn-reject" class=icon-delete]Reject[/url][/div]';
}
}
$this->redButtons[BUTTON_GUIDE_LOG] = true;
$this->redButtons[BUTTON_GUIDE_REPORT] = $this->subject->canBeReported();
$this->infobox = $qf ?? '';
$this->author = $this->subject->getField('author'); // add to g_pageInfo in GenericPage:prepareContent()
if ($this->subject->userCanView())
$this->redButtons[BUTTON_GUIDE_EDIT] = User::canWriteGuide() && $this->subject->getField('status') != GUIDE_STATUS_ARCHIVED;
// the article text itself is added by GenericPage::addArticle()
}
private function displayChangelog() : void
{
$this->addScript([SC_JS_STRING, '
$(document).ready(function() {
var radios = $("input[type=radio]");
function limit(col, val) {
radios.each(function(i, e) {
if (col == e.name)
return;
if (col == "b")
e.disabled = (val <= parseInt(e.value));
else if (col == "a")
e.disabled = (val >= parseInt(e.value));
});
};
radios.each(function (i, e) {
e.onchange = limit.bind(this, e.name, parseInt(e.value));
if (i < 2 && e.name == "b") // first pair
$(e).trigger("click");
else if (e.value == 0 && e.name == "a") // last pair
$(e).trigger("click");
});
});
']);
$buff = '<ul>';
$inp = fn($rev) => User::isInGroup(U_GROUP_STAFF) ? ($rev !== null ? '<input name="a" value="'.$rev.'" type="radio"/><input name="b" value="'.$rev.'" type="radio"/><b>' : '<b style="margin-left:28px;">') : '';
$logEntries = DB::Aowow()->select('SELECT a.`displayName` AS `name`, gcl.`date`, gcl.`status`, gcl.`msg`, gcl.`rev` FROM ?_guides_changelog gcl JOIN ?_account a ON a.`id` = gcl.`userId` WHERE gcl.`id` = ?d ORDER BY gcl.`date` DESC', $this->typeId);
foreach ($logEntries as $log)
{
if ($log['status'] != GUIDE_STATUS_NONE)
$buff .= '<li class="guide-changelog-status-change">'.$inp($log['rev']).Lang::guide('clStatusSet', [Lang::guide('status', $log['status'])]).Lang::main('colon').'</b>'.Util::formatTimeDiff($log['date'])."</li>\n";
else if ($log['msg'])
$buff .= '<li>'.$inp($log['rev']).Util::formatTimeDiff($log['date']).Lang::main('colon').'</b>'.$log['msg'].' <i class="q0">'.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."</i></li>\n";
else
$buff .= '<li class="guide-changelog-minor-edit">'.$inp($log['rev']).Util::formatTimeDiff($log['date']).Lang::main('colon').'</b><i>'.Lang::guide('clMinorEdit').'</i> <i class="q0">'.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."</i></li>\n";
}
// append creation
$buff .= '<li class="guide-changelog-created">'.$inp(0).'<b>'.Lang::guide('clCreated').Lang::main('colon').'</b>'.Util::formatTimeDiff($this->subject->getField('date'))."</li>\n</ul>\n";
if (User::isInGroup(U_GROUP_STAFF))
$buff .= '<input type="button" value="Compare" onclick="alert(\'NYI\');"/>';
$this->name = lang::guide('clTitle', [$this->typeId, $this->subject->getField('title')]);
$this->extraHTML = $buff;
}
private function initNew() : void
{
$this->addScript(
[SC_JS_FILE, 'js/article-description.js'],
[SC_JS_FILE, 'js/article-editing.js'],
[SC_JS_FILE, 'js/guide-editing.js'],
[SC_JS_FILE, 'js/fileuploader.js'],
[SC_JS_FILE, 'js/toolbar.js'],
[SC_JS_FILE, 'js/AdjacentPreview.js'],
[SC_CSS_FILE, 'css/article-editing.css'],
[SC_CSS_FILE, 'css/fileuploader.css'],
[SC_CSS_FILE, 'css/guide-edit.css'],
[SC_CSS_FILE, 'css/AdjacentPreview.css'],
[SC_CSS_STRING, '#upload-result input[type=text] { padding: 0px 2px; font-size: 12px; }'],
[SC_CSS_STRING, '#upload-result > span { display:block; height: 22px; }'],
[SC_CSS_STRING, '#upload-result { display: inline-block; text-align:right; }'],
[SC_CSS_STRING, '#upload-progress { display: inline-block; margin-right:8px; }']
);
$this->articleUrl = 'new';
$this->tpl = 'guide-edit';
$this->name = Lang::guide('newTitle');
Lang::sort('guide', 'category');
$this->typeId = 0; // signals 'edit' to create new guide
}
private function initEdit() : bool
{
$this->addScript(
[SC_JS_FILE, 'js/article-description.js'],
[SC_JS_FILE, 'js/article-editing.js'],
[SC_JS_FILE, 'js/guide-editing.js'],
[SC_JS_FILE, 'js/fileuploader.js'],
[SC_JS_FILE, 'js/toolbar.js'],
[SC_JS_FILE, 'js/AdjacentPreview.js'],
[SC_CSS_FILE, 'css/article-editing.css'],
[SC_CSS_FILE, 'css/fileuploader.css'],
[SC_CSS_FILE, 'css/guide-edit.css'],
[SC_CSS_FILE, 'css/AdjacentPreview.css'],
[SC_CSS_STRING, '#upload-result input[type=text] { padding: 0px 2px; font-size: 12px; }'],
[SC_CSS_STRING, '#upload-result > span { display:block; height: 22px; }'],
[SC_CSS_STRING, '#upload-result { display: inline-block; text-align:right; }'],
[SC_CSS_STRING, '#upload-progress { display: inline-block; margin-right:8px; }']
);
$this->articleUrl = 'edit';
$this->tpl = 'guide-edit';
$this->name = Lang::guide('editTitle');
$this->save = $this->_post['save'] || $this->_post['submit'];
// reject inconsistent guide data
if ($this->save)
{
// req: set data
if (!$this->_post['title'] || !$this->_post['name'] || !$this->_post['body'] || $this->_post['locale'] === null)
return false;
// req: valid data
if (!in_array($this->_post['category'], $this->validCats) || !(Cfg::get('LOCALES') & (1 << $this->_post['locale'])))
return false;
// sanitize: spec / class
if ($this->_post['category'] == 1) // Classes
{
if ($this->_post['classId'] && !ChrClass::tryFrom($this->_post['classId']))
$this->_post['classId'] = 0;
if (!in_array($this->_post['specId'], [-1, 0, 1, 2]))
$this->_post['specId'] = -1;
if ($this->_post['specId'] > -1 && !$this->_post['classId'])
$this->_post['specId'] = -1;
}
else
{
$this->_post['classId'] = 0;
$this->_post['specId'] = -1;
}
}
if ($this->_get['id']) // edit existing guide
{
$this->typeId = $this->_get['id']; // just to display sensible not-found msg
if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `id` = ?d AND `status` <> ?d {AND `userId` = ?d}', $this->typeId, GUIDE_STATUS_ARCHIVED, User::isInGroup(U_GROUP_STAFF) ? DBSIMPLE_SKIP : User::$id))
$this->typeId = intVal($id);
}
else if ($this->_get['id'] === 0) // create new guide and load in editor
$this->typeId = DB::Aowow()->query('INSERT INTO ?_guides (`userId`, `date`, `status`) VALUES (?d, ?d, ?d)', User::$id, time(), GUIDE_STATUS_DRAFT);
return $this->typeId > 0;
}
protected function editorFields(string $field, bool $asInt = false) : string|int
{
return $this->editorFields[$field] ?? ($asInt ? 0 : '');
}
protected function generateTooltip()
{
$power = new \StdClass();
if (!$this->subject->error)
{
$power->{'name_'.Lang::getLocale()->json()} = strip_tags($this->name);
$power->{'tooltip_'.Lang::getLocale()->json()} = $this->subject->renderTooltip();
}
return sprintf($this->powerTpl, Util::toJSON($this->articleUrl ?: $this->typeId), Lang::getLocale()->value, Util::toJSON($power, JSON_AOWOW_POWER));
}
protected function generatePath() : void
{
if ($x = $this->subject?->getField('category'))
$this->path[] = $x;
}
protected function generateTitle() : void
{
if ($this->show == self::SHOW_EDITOR)
array_unshift($this->title, Lang::guide('editTitle').Lang::main('colon').$this->subject->getField('title'), Lang::game('guides'));
if ($this->show == self::SHOW_NEW)
array_unshift($this->title, Lang::guide('newTitle'), Lang::game('guides'));
else
array_unshift($this->title, $this->subject->getField('title'), Lang::game('guides'));
}
protected function postCache() : void
{
// increment views of published guide; ignore caching
if ($this->subject?->getField('status') == GUIDE_STATUS_APPROVED)
DB::Aowow()->query('UPDATE ?_guides SET `views` = `views` + 1 WHERE `id` = ?d', $this->typeId);
}
protected static function checkDescription(string $str) : string
{
// run checkTextBlob and also replace \n => \s and \s+ => \s
$str = preg_replace(parent::PATTERN_TEXT_BLOB, '', $str);
$str = strtr($str, ["\n" => ' ', "\r" => ' ']);
return preg_replace('/\s+/', ' ', trim($str));
}
}
?>