569 Commits

Author SHA1 Message Date
Sarjuuk
10ef33f709 Spells/SpellClick
* evaluate npc_spellclick_data and display on Spell and NPC Detail Pages
 * also categorize these spells as NPC-spells
 * closes #438
2025-11-21 22:47:43 +01:00
Sarjuuk
ae1b6c59b1 Quests/Requisites
* fixed case where an exclusiveGroup of 0 wasn't considered
 * closes #456
2025-11-21 21:09:09 +01:00
Sarjuuk
b764200c2a SmartAI/Conditions
* embed Conditions into SmartAI table so we can evaluate CONDITION_SOURCE_TYPE_SMART_EVENT (22)
 * make SmartAI table display flexible
2025-11-20 23:50:23 +01:00
Sarjuuk
c0454917ac Localization/Typo
* fix acc creation prompt for locale ES
2025-11-20 23:50:23 +01:00
Sarjuuk
b3e215cc40 Site/layout
* increase max width of layout from 1340px to 1640px
2025-11-19 20:30:57 +01:00
Sarjuuk
d4694cd2db User/Reputation
* fix storing negative reputation values
2025-11-19 20:30:57 +01:00
Sarjuuk
a5051c9bf5 Loot/Modes
* work against more correctly assigning instance mode to entities and loot
    - added manually collected data for difficulty versions of gameobjects, just boss chests for now.
      update setup/source to default object source to base difficulty version if able
    - update spelldifficulty table to contain the (likely) mapmode it will be used in
  * refactored class loot
    - implement loot mode indicators on listview for creature and gameobject loot
    - show 'drops' listview tab on instance zone page
    - fixes against tribute chest systems (toc / ulduar)
    - fix icc gunship battle chest ownership
2025-11-19 20:22:33 +01:00
Sarjuuk
be3701df91 Params/Fixup
* FILTER_SANITIZE_URL is absurdly strict and will not tolerate umlauts or spaces
   replaced with printable chars regex
2025-11-19 17:33:16 +01:00
Sarjuuk
9b905883df Listview/Conditions
* make column width flex
2025-11-19 16:59:57 +01:00
Sarjuuk
9db3e766da QuestDetailPage/ShowOnMap
* increase strictness for sources of required items shown on mapper from 1% to 5%
2025-11-19 16:59:57 +01:00
Sarjuuk
7f29c1d4b7 CurrencyDetailPage/Tabs
* add currency column to display gains
2025-11-19 16:59:57 +01:00
Sarjuuk
57665aaa9e ItemDetailPage/Misc
* fix "vendor in" mapper
 * add "fished in" mapper
 * move 'see-also' and 'same-model-as' tabs to the back of the tabs list
2025-11-19 16:59:57 +01:00
Sarjuuk
4cb544182d Setup/Spawns
* evaluate waypoint paths linked via creature_template_addon
2025-11-15 22:17:09 +01:00
Sarjuuk
a2b87da285 Localization/CN
* fix excess whitespaces betweeen number and unit
2025-11-15 22:17:03 +01:00
Sarjuuk
03bab92cb8 DateTime/Fixup
* fix displaying expected '0 seconds' instead of 'n/a' or '1 ms'
2025-11-15 22:16:58 +01:00
Sarjuuk
103287f91b Setup/Pets
* move custom data from pet script to db
2025-11-15 22:16:53 +01:00
Sarjuuk
82f36fd342 Setup/Source
* generally flag items of quality artifact as unavailable
 * 04f3aa7a82 caused some items transformed by spell to be 'available'
2025-11-15 22:16:48 +01:00
Sarjuuk
f5654ae21f DateTime
* recreate date functions from javascript in new class DateTime
 * move date and time functions from Util to new class
 * fixes various cooldown messages for account recovery
2025-11-14 19:16:12 +01:00
Sarjuuk
1fe3690244 Cache/Fixup
* fix cache collision on list pages caused by improper encoding of category
 * fix cache key not encoding category values of int: 0
 * version bump to flush caches
2025-11-13 21:29:35 +01:00
Sarjuuk
45417122c2 UserPage/Fixup
* only show related heading if we have tabs to display
2025-11-11 20:35:42 +01:00
Sarjuuk
7cf5dded98 Signatures
* add non-functional endpoint stubs with info
2025-11-11 20:17:39 +01:00
Sarjuuk
31f51276b2 Profiler/Fixup
* fix exception when manually querying for unsynced guild/arena-team
   that shares it's name with another guild/team (e.g. ìíîi is the same to SQL)
   and the target guild/team not being the first result
2025-11-10 20:31:09 +01:00
Sarjuuk
643c3c2a83 Comments/Goto
* fix comment links in reputation history on user page
2025-11-10 18:45:39 +01:00
Sarjuuk
a135dfce90 Profiler/Completions
* add keyed col `exalted` to reputation completions table to speed up lookups
2025-11-09 19:05:33 +01:00
Sarjuuk
0d42d2a2c4 UserPage/Optimization
* split up fetching of custom profiles and characters to make use of existing keys
2025-11-09 16:22:26 +01:00
Sarjuuk
fa89a5ad1e User/Fixup
* fix fetching user characters, borked in 474b5b5aec062b61e8d707c91739b50ad77e81ef
 * take #2
2025-11-09 16:22:00 +01:00
Sarjuuk
6eb5a67add Profiler/Optimization
* move searchable flags to their own db cols to speed up lookups
 * don't cast profile name to LOWER in SQL when displaying tooltips.
2025-11-09 16:20:52 +01:00
Sarjuuk
8a169eb400 Setup/Fixup
* catch error if url from self test is unreachable
2025-11-09 16:16:28 +01:00
Sarjuuk
cf4e8a527c User/Fixup
* fix fetching user characters, borked in 474b5b5aec062b61e8d707c91739b50ad77e81ef
2025-11-08 18:07:26 +01:00
Sarjuuk
48564ab8b5 Misc/Fixup
* fix building num ranges, added in 8212811970
2025-11-07 21:19:43 +01:00
Sarjuuk
edc297f97a NPCs/Fixup
* fix parent npcs name for locales CN and ES
2025-11-07 20:47:39 +01:00
Sarjuuk
5d02a20719 SQL/Misc
* add keys to spells table to speed up related spells queries
2025-11-07 20:34:49 +01:00
Sarjuuk
f44de66de7 User/Profiles
* speed up load of user profiles
2025-11-07 20:34:47 +01:00
Sarjuuk
16c5b73cd3 User/Misc
* don't run querys if not strictly required (e.g. query for chars from ajax context)
 * prepare user globals and favorites so errors can be handled and don't destroy the template
   this also allows for profiling of the affected queries
 * add keys to items table to speed up querying for recipes in general and user completions in particular
2025-11-05 15:39:28 +01:00
Sarjuuk
9020e36db6 SmartAI/Misc
* make errors more verbose if SAI tries to set unexpected flags
 * do not escape strings. By now thats handled by Frontend/Markup
2025-11-04 19:48:26 +01:00
Sarjuuk
597898450d WorldEvent/Misc
* fix excess colons in tooltip
 * fix advancing date window while event is still active
2025-11-04 19:48:19 +01:00
Sarjuuk
6a94888686 Filter/Fixup
* try to prune deselected criteria/weight selectors from filter input
2025-11-04 00:04:24 +01:00
Sarjuuk
e3d6f7b3a7 Profiler/Completions
* show completion info for claimed characters in infobox on
      appropriate db pages
2025-11-03 20:50:54 +01:00
Sarjuuk
37380ff515 Frontend/InfoboxMarkup
* you can now pass attributes to the [li] element
2025-11-03 18:47:06 +01:00
Sarjuuk
8212811970 Misc/Cleanup
* create function for num range .. creation
2025-11-01 21:01:03 +01:00
Sarjuuk
1e9e406ff0 TextResponse/Fixup
* make class non-abstract so we can generate a 403/404 message on base
2025-10-31 16:44:37 +01:00
Sarjuuk
3984bd0ae2 Spells/Misc
* limit chance/ppm precision on spell procs chances
 * do not apply a spells EffectXBonusMultiplier for physical spells
2025-10-29 15:51:23 +01:00
Sarjuuk
441ad38543 Filter/Locales
* fix embedding and triggering fi_toggle on filtrable listviews for locale zhCN
2025-10-28 19:58:47 +01:00
Sarjuuk
88cc76feae Localization/Filters
* add missing spell effects & aura names
 * don't return null for unused effects/auras (triggers an error)
2025-10-28 19:58:44 +01:00
Sarjuuk
96c777191d Spells/Scaling
* move scaling data to the appropriate spell effects (like WH)
2025-10-27 21:19:40 +01:00
Sarjuuk
40b2830cad Spells/Scaling
* hopefully fix a lot of nonsensical spell scaling infos
 * note: an aweful lot of physical spells are hardcoded or have spell scripts and won't display any info
2025-10-27 19:44:00 +01:00
Sarjuuk
9741774683 Spells/Reagents
* fix reagents listing and spell tooltips for nonexistent reagents
2025-10-27 17:02:30 +01:00
Sarjuuk
e8bc37f82f Filter/NPC
* fix react filter
 * remove excess colons
2025-10-27 01:47:27 +01:00
Sarjuuk
7cbe1f6007 Reports/Fixup
* also include source url when checking target context
 * cleanup source url to be usable as key
2025-10-26 19:19:50 +01:00
Sarjuuk
3a25c2390f Listviews/AddIns
* AddIns must be output directly before the listview it is used by
2025-10-26 17:32:18 +01:00
Sarjuuk
cf2ace805b Spawns/Fixup
* fix maps for single-floor dungeons borked in 33cd290dc3
2025-10-24 18:29:41 +02:00
Sarjuuk
2f8e035783 Search/Fixup
* fix redirecting to result on exact hit
2025-10-24 18:28:25 +02:00
Sarjuuk
1a55b30766 UserPage/Fixup
* move description inside text container
 * add missing 'related' heading
2025-10-23 20:49:58 +02:00
Sarjuuk
6d7f9c0f00 Quest/Fixup
* don't try to create an objective for empty SourceItem
 * fixed size of source spell icon
2025-10-23 16:26:59 +02:00
Sarjuuk
862b3dff73 IconElement/Fixup
* do not change type of num / qty params ('+1' is not numeric)
2025-10-23 00:48:04 +02:00
Sarjuuk
b1f22f7e68 Spells/ExtraLoot
* display extra loot from skill_extra_item_template like perfect gems
 * for specializations, display affected spells in Bonus Loot tab
 * cleanup subject->id to typeId
 * closes #286
2025-10-23 00:10:52 +02:00
Sarjuuk
1d922c1147 Locks
* implemented display of LOCK_TYPE_SPELL (3 cases)
 * show "unlocks" tab on spell detail page
 * closes #288
2025-10-22 22:18:27 +02:00
Sarjuuk
f9ace6a671 Spell/Sources
* always display quest source from RewardSpellCast
 * fix inherited quest sources via learn spells
 * closes #353
2025-10-22 18:57:33 +02:00
Sarjuuk
6ea1457c4f Guides/Fixup
* make preview area of class: text so styles are applied as expected
 * fix evaluation of Markup h2/h3 attribute toc=false, so headings are excluded from TOC as expected
2025-10-22 18:06:38 +02:00
Sarjuuk
f522c960d9 Modelviewer/Fixup
* fix buttons of lightbox
2025-10-22 17:08:36 +02:00
Sarjuuk
1365cdb261 Spell/Effects
* show spells affected by SPELL_AURA_IGNORE_COMBAT_RESULT
2025-10-22 01:57:58 +02:00
Sarjuuk
9b591e7a3a Items/Tooltips
* fix itemId to scientific notation conversion, when itemId was joined
   by an enchantmentId (1234e56 => 1.234e53), breaking tooltip display.
2025-10-21 23:35:42 +02:00
Sarjuuk
6da71afc68 Filter/Fixup
* allow unicode chars when checking GET param
2025-10-21 20:53:27 +02:00
Sarjuuk
033a9181ae Profiler/Localization
* TCs guild and arena_team tables as encoded as utf8mb4_general_ci,
   which is not accent-aware. So we have to get all results and filter
   for the correct one in php.
 * fixes an issue where direcly accessing a guild/arena-team whith a name
   simiar to an already known guild/team would lookup the wrong subject
   on the server and then fail to create a local stub with already existing key.
   (Shâdów vs Shadow)
2025-10-21 17:57:00 +02:00
Sarjuuk
1dcc9363da Profiler/Filter
* fix filter params being propagated between different profiler types
   (e.g. arena team size filter being appended to guilds menu)
2025-10-21 15:35:28 +02:00
Sarjuuk
51b6e29316 Profiler/Queue
* send ready status for characters/guilds/arena-teams whose resync
   cooldown hasn't expired yet
2025-10-20 20:38:15 +02:00
Sarjuuk
14c159c164 Setup/Factions
* fix switched base rep field indizes, causing Profiler to miscalculate
   character standing
 * replace hardcoded sql table prefixes
2025-10-20 19:23:52 +02:00
Sarjuuk
33cd290dc3 Setup/Spawns
* fix coords for cases with coords in both WorldMapArea.dbc and
   DungeonMap.dbc without using WorldMapArea.dbc as base floor
2025-10-20 19:23:02 +02:00
Sarjuuk
2e029f3d96 Profiler/Talents
* fix building talent string for hunter pets.
   the alternate spells (e.g. Dash & Swoop) must both be included
 * align talent order in build scripts talenticons, talentcalc with
   Profiler talent string builder
 * fix Shamans gaining 5% Parry by talent Spirit Weapons
 * cleanup
2025-10-20 16:25:24 +02:00
Sarjuuk
6a32c770cd Profiler/Pets
* catch error case where a player owns a pet that is no longer tameable/has no pet family
2025-10-19 22:46:08 +02:00
Sarjuuk
4d421d2bbb Filter/Errors
* handle stat weights quirk, analogous to the criteria quirk
2025-10-18 16:46:25 +02:00
Sarjuuk
176cf137fb Filter/Fixup
* criteria parameters can be placeholder/null
2025-10-17 14:33:52 +02:00
Sarjuuk
830edb8265 PageTemplate/Fixup
* use get_object_vars() instead of property_exists() to test if we can
   load a variable from provided context. The former only returns
   accessible vars while the latter returns true for all properties.
2025-10-16 02:14:47 +02:00
Sarjuuk
7d8b524478 Listview/Cost
* do not append 0 achievementpoints to cost builder. It will be displayed
2025-10-15 21:53:00 +02:00
Sarjuuk
95918c0410 SoundDetailPage/Fixup
* fix exception when assigning WorldState conditions to the zones tab
2025-10-15 01:06:57 +02:00
Sarjuuk
a275955ee3 Admin/Config
* handle Ajax errors
2025-10-15 00:26:05 +02:00
Sarjuuk
37beaa2db5 Filter/Errors
* move checks to __construct so they can be run on $_POST data
   and don't create malformed filter urls
 * if we received malformed $_GET params, build new params and reload
 * do not store error state in cache
 * cleanup
2025-10-15 00:05:55 +02:00
Sarjuuk
c0097f3987 Mapper/Objectives
* fix display of item objectives by making LocString JsonSerializable
2025-10-14 16:17:59 +02:00
Sarjuuk
92c58cc5d1 Localization/UIEscapes
* fixed expanding |2 placeholder in general and when the referenced word itself was a placeholder ($N => <nom>)
 * fixed expanding |3 placeholder for caseIds > 9
2025-10-13 20:37:39 +02:00
Sarjuuk
04f3aa7a82 Setup/Source
* respect disabled Quests and Spells when flagging Items as unavailable
 * reuse data from loot_link to set difficuly bits and zoneId
   for loot container GOs
2025-10-12 22:32:52 +02:00
Sarjuuk
65d490a8ae Enchantments/Stats
* entirely forgo ?_item_stats table when calculating enchantment stats
2025-10-12 22:24:09 +02:00
Sarjuuk
816eacaf73 Summary/Fixup
* allow signed integers (random enchantments) in summary definition
2025-10-12 22:24:03 +02:00
Sarjuuk
034eca1f58 Items/RandEnchants
* fix amount calculation for scaling enchantments
 * cache RandomPropPoints lookups
2025-10-12 22:22:56 +02:00
Sarjuuk
a33abb84fe Setup/Account
* don't overwrite existing account in case of email conflict
2025-10-12 17:48:06 +02:00
Sarjuuk
fb7b22db36 Account/Passwords
* use buildin php functions to handle passwords
 * increase cost of BCRYPT
 * make use of the SensitiveParameter attribute
2025-10-12 17:48:06 +02:00
Sarjuuk
dd838fa994 Misc/Doc
* add several ItemMods unusd by client but still found in item_template as comment
2025-10-12 00:57:09 +02:00
Sarjuuk
d32074fdcd ZoneDetailPage/Tabs
* only offer 'filter result' prompt on tabs for zones that are filtrable
2025-10-12 00:15:59 +02:00
Sarjuuk
494061de82 Cache/Fixup
* correct miscData offset for tooltips introduced in 3edac3c77a
 * fix generating cache key for item upgrade searches
2025-10-12 00:15:52 +02:00
Sarjuuk
9b0aa5c885 Achievements/Fixup
* fix fetching achievements from child catgs if selected catg is empty
2025-10-12 00:15:28 +02:00
Sarjuuk
40e98081c9 LatestComments/RSS
* fix url format for replies
2025-10-12 00:15:15 +02:00
Sarjuuk
77f2a0c21d Profiler/Resync
* fix logging ids on resync failure
2025-10-10 22:33:31 +02:00
Sarjuuk
465e019eaa FactionDetailPage/Tabs
* only offer 'filter result' prompt on tabs for factions that are filtrable
2025-10-10 22:33:25 +02:00
Sarjuuk
63053757c9 Guides/Fixup
* fix showing wrong guide version to staff
 * fix sticky icon offset
2025-10-10 20:53:56 +02:00
Sarjuuk
a96f6c4cdf PageTemplate/Fixup
* really fix merging jsGlobals from comments/etc. into existing PageTemplate
2025-10-10 20:49:56 +02:00
Sarjuuk
b832fc172c Items/Gearscore
* fix warning in GS calculation
2025-10-10 20:49:43 +02:00
Sarjuuk
196f60f176 Filter/Zones
* add missing Ruby Sanctum to zones dropdown
 * sort zones alphabetically
2025-10-10 20:49:32 +02:00
Sarjuuk
204d4b8ae2 Profiler/Sync
* fix SQL FK error when creating guild or arenateam stub.
 * Urlized name field is non-optional
2025-10-10 20:49:14 +02:00
Sarjuuk
5d2fd00358 Profiler/Save
* fixed inventory definitions not allowing for negative ids (random enchantments)
 * added handling invalid inventory definitions
2025-10-10 20:49:07 +02:00
Sarjuuk
1dcdf9623b Endpoints/User
* do not display user page for internal system user
2025-10-08 20:02:48 +02:00
Sarjuuk
215ad39cc6 NPC/Reputation
* try to fix reputation spillover
 * fix extra colon on reputation gains
2025-10-08 19:48:36 +02:00
Sarjuuk
a9ed897ea6 Filter/Fixup
* fix evaluating imbalanced criteria
2025-10-08 19:48:29 +02:00
Sarjuuk
3edac3c77a Endpoints/Cache
* fix cache id collision when category == dbTypeId for a given dbType
 * increment version number to invalidate existing caches
 * maps endpoint doesn't need caching. It is entirely static content.
2025-10-08 19:48:21 +02:00
Sarjuuk
95ee9d2c25 NPCs/Fixup
* fix exception when displaying NPC with elemental resistances in base
   version but not difficulty modes
2025-10-07 15:51:07 +02:00
Sarjuuk
d79742d599 Profiler/Fixup
* dont use unsynced profile stubs in attained % calculation
2025-10-06 23:23:40 +02:00
Sarjuuk
e300086cc8 IconElement/Fixup
* a DOMElements text value must be escaped manually
   (e.g. Foror &amp; Tigule)
2025-10-06 23:23:30 +02:00
Sarjuuk
a7e9ac2cf2 Misc/Fixup
* HTTP_USER_AGENT is not guaranteed to be set
2025-10-06 17:16:41 +02:00
Sarjuuk
05f5b0ed34 Response/Params
* so we can't directly use BackedEnum::tryFrom as validator, because if
   the Enum is of <int> and the string is not what php considers numeric,
   we get a straight TypeError Exception instead of null for failing the tryFrom.
2025-10-06 17:06:49 +02:00
Sarjuuk
e37620c01b Search/Fixup
* fix pruning empty tokens from search
2025-10-06 17:06:43 +02:00
Sarjuuk
c40bd3851b Profiler/Fixup
* fix scoring perm enchantments
2025-10-06 17:06:34 +02:00
Sarjuuk
452615a92d Filters/Misc
* be a bit more lenient on level inputs
 * fix displaying array of requirements on error
2025-10-06 17:06:25 +02:00
Sarjuuk
704894c1e3 Spells/Fixup
* skillLines can be empty for unused glyphs etc.
2025-10-06 17:06:14 +02:00
Sarjuuk
045c16c241 PageTemplate/Profiler
* don't skip running parent::generate for incomplete profiles
2025-10-06 15:00:47 +02:00
Sarjuuk
7b429811a9 Defines/Races
* add unplayable races to ChrRace enum so RaceDetailPage can display them.
 * don't show empty icons for unplayable races
2025-10-05 20:20:40 +02:00
Sarjuuk
aa7c0186fc Profiler/Cleanup
* gracefully handle DB errors when fetching realms instead of crashing
2025-10-05 20:19:48 +02:00
Sarjuuk
7b752143a0 Creature/Quotes
* don't add superfluous creature name placeholder to emotes
2025-10-05 18:55:39 +02:00
Sarjuuk
baf4ba5b98 Codestyle/Cleanup 2025-10-05 00:28:27 +02:00
Sarjuuk
eb95b03e31 SmartAI/Update
* implement events added in 3bb4f56773
2025-10-04 20:02:34 +02:00
Sarjuuk
4fe35d9e3c PageTemplate/Fixup
* fix merging jsGlobals from comments/etc. into existing PageTemplate
2025-10-04 17:13:45 +02:00
Sarjuuk
5355989015 Logging/Misc
* don't log passwords to DB (and neither check_passwords)
2025-10-04 17:13:34 +02:00
Sarjuuk
9fc84cdf9e Comments/Fixup
* fix false error when voting on relies
2025-10-04 15:39:44 +02:00
Sarjuuk
a6108be400 Spells/Parsing
* bandaid fix parsing deeply nested formulas in non-interactive mode
   (should rethink how/when formulas get flagged as un-evalable)
2025-10-04 01:10:38 +02:00
Sarjuuk
ff690770b5 Misc/Fixup
* type error when declaring listview
2025-10-04 00:15:38 +02:00
Sarjuuk
6263ccd92a SkillDetailPage/Tabs
* add tab for spells modifying skill value
2025-10-03 18:32:27 +02:00
Sarjuuk
60eb816002 Quickfacts
* where applicable:
   - show typeId
   - show english name if currently localized
   - show percentage attained by synced profiles
 * fixed some typos
2025-10-03 17:49:50 +02:00
Sarjuuk
bc112b2b16 Template/Fixup
* fix directly adding dataloader to PageTemplate
2025-10-03 17:49:49 +02:00
Sarjuuk
6d86f880f4 Analytics/Cookies
* don't ask users to consent on GA tracking if we don't use GA tracking
2025-10-02 19:53:53 +02:00
Sarjuuk
bd1f139c2e CLI/Misc
* handle error case where setup is run automated and receives no input on STDIN
2025-10-02 16:16:49 +02:00
Yrito
36aa33ac26 Setup/CLI
* Allow starting setup at specific step
2025-10-02 14:35:13 +02:00
Sarjuuk
647be4a946 Setup/SQL
* move sql files into its own folder.
 * move outdated updates out of the update folder, sorted by github tags
 * split up the db dump so my editor doesn't try to hang itself if i dare to touch that file
2025-09-25 18:44:48 +02:00
Sarjuuk
92c1c59d3a Template/Update (Cleanup)
* smush leftover changes into a commit
 * create fresh db dump, without dbc placeholders
 * version bump

 ... all done
2025-09-25 16:01:16 +02:00
Sarjuuk
6557e70d5c Template/Update (Part 47)
* split global.js into its components, so it can be reasonably processed by setup
 * make reputation requirements configurable
 * move Markup and Locale back into global.js (removed associated build scripts)
 * extend Icon to display iconId in lightbox popup
2025-09-25 16:01:14 +02:00
Sarjuuk
a48e94cd8b Template/Update (Part 46 - VI)
* account management rework: Delete account
2025-09-25 16:01:06 +02:00
Sarjuuk
1d5539b362 Template/Update (Part 46 - V)
* account management rework: Avatar functionality
 * show avatar at comments (beckported, because no forums)
2025-09-25 16:01:04 +02:00
Sarjuuk
258ac19f0a Template/Update (Part 46 - IV)
* account management rework: Personal Settings functionality
 * email, password, username update
 * email updates now also mails the old address for confirmation
2025-09-25 16:00:47 +02:00
Sarjuuk
8fadce88ad Template/Update (Part 46 - III)
* account management rework: Recovery Options
2025-09-25 16:00:36 +02:00
Sarjuuk
f16479b50c Template/Update (Part 46 - II)
* account management rework: Signup functionality
2025-09-25 16:00:36 +02:00
Sarjuuk
155bf1e4a3 Template/Update (Part 46 - I)
* account management rework: Base
 * create proper account settings page
   - modelviewer preferences
   - show ids in lists
   - announcement purge
   - public description
  * fix broken FKs between aowow_user_ratings and aowow_account
2025-09-25 16:00:30 +02:00
Sarjuuk
ab27976132 Template/Update (Part 45)
* convert misc admin endpoints
2025-09-25 15:59:11 +02:00
Sarjuuk
3f8a1838c0 Template/Update (Part 44)
* convert admin - config
 * test for presence of Memcached on enable
 * allow self-signed certs on domain self test
2025-09-25 15:59:09 +02:00
Sarjuuk
f1b613cfa0 Template/Update (Part 43)
* split 'arena-teams' into separate endpoints
2025-09-25 15:58:56 +02:00
Sarjuuk
398ff16b65 Template/Update (Part 42)
* split 'guilds' into separate endpoints
2025-09-25 15:58:56 +02:00
Sarjuuk
69df20af63 Template/Update (Part 41)
* split 'profiler' into separate endpoints
 * implement profile=avatar endpoint (though it doesn't do a whole lot and isn't referenced (see comments))
2025-09-25 15:58:56 +02:00
Sarjuuk
fef27c58e6 Template/Update (Part 40)
* convert 'guides' (listing, viewing, writing & management)
 * don't allow comments on WIP guides
2025-09-25 15:58:56 +02:00
Sarjuuk
cb523353fd Template/Update (Part 39)
* implement video suggestion & management
2025-09-25 15:58:52 +02:00
Sarjuuk
a369244908 Template/Update (Part 38)
* split Screenshot upload & management into separate endpoints
 * move shared functions to manager classes
 * cleanup javascript
 * move test for config screenshot min size to cfg class
2025-09-25 15:58:17 +02:00
Sarjuuk
3d3e2211e5 Template/Update (Part 37)
* convert dbtype 'sound'
2025-09-25 15:56:51 +02:00
Sarjuuk
3f8d5d90e1 Template/Update (Part 36)
* convert dbtype 'mail'
2025-09-25 15:56:51 +02:00
Sarjuuk
0cf9069eb1 Template/Update (Part 35)
* convert dbtype 'icon'
 * improve on IconlistFilter
2025-09-25 15:56:50 +02:00
Sarjuuk
1672883186 Template/Update (Part 34)
* convert dbtype 'areatrigger'
2025-09-25 15:56:50 +02:00
Sarjuuk
3f7f522d50 Template/Update (Part 33)
* convert dbtype 'zone'
2025-09-25 15:56:50 +02:00
Sarjuuk
d66a863f55 Template/Update (Part 32)
* convert dbtype 'event'
2025-09-25 15:56:50 +02:00
Sarjuuk
e876463f3b Template/Update (Part 31)
* convert dbtype 'quest'
 * make use of separate GlobalStrings for spell rewards
2025-09-25 15:56:50 +02:00
Sarjuuk
253cbcb4d9 Template/Update (Part 30)
* convert dbtype 'object'
2025-09-25 15:56:50 +02:00
Sarjuuk
f17b4f58bf Template/Update (Part 29)
* convert dbtype 'npc'
2025-09-25 15:56:50 +02:00
Sarjuuk
a824bb106c Template/Update (Part 28)
* convert dbtype 'faction'
2025-09-25 15:56:50 +02:00
Sarjuuk
79c937e0a3 Template/Update (Part 27)
* convert dbtype 'currency'
2025-09-25 15:56:49 +02:00
Sarjuuk
f76869ecbe Template/Update (Part 26)
* convert dbtype 'title'
2025-09-25 15:56:49 +02:00
Sarjuuk
e6980ce220 Template/Update (Part 25)
* convert dbtype 'spell'
 * point spell effects on detail page to spells filter
2025-09-25 15:56:49 +02:00
Sarjuuk
3ba0cc4ade Template/Update (Part 24)
* convert dbtype 'race'
2025-09-25 15:56:48 +02:00
Sarjuuk
64ef350e0d Template/Update (Part 23)
* convert dbtype 'skill'
2025-09-25 15:56:48 +02:00
Sarjuuk
cdb7e1e7ec Template/Update (Part 22)
* convert dbtype 'pet'
2025-09-25 15:56:48 +02:00
Sarjuuk
2dd9265700 Template/Update (Part 21)
* convert dbtype 'emotes'
 * in setup use voicemacros as additional aliasses
 * also fix emote text descriptor
2025-09-25 15:56:47 +02:00
Sarjuuk
e33bc9117c Template/Update (Part 20)
* convert dbtype 'class'
2025-09-25 15:56:47 +02:00
Sarjuuk
98a54cd871 Template/Update (Part 19)
* convert dbtype 'achievement'
2025-09-25 15:56:47 +02:00
Sarjuuk
26226e2bad Template/Update (Part 18)
* convert dbtype 'item'
 * StatsContainer::toJson - exclude empty values in listviews + xml
2025-09-25 15:56:47 +02:00
Sarjuuk
11bb5a521b Template/Update (Part 17)
* convert dbtype 'itemset'
2025-09-25 15:56:47 +02:00
Sarjuuk
70e4bca10f Template/Update (Part 16)
* convert amalgamation utility.php into separate endpoints
2025-09-25 15:56:47 +02:00
Sarjuuk
503b9458e0 Template/Update (Part 15)
* convert comment/reply ajax (add, edit, delete, vote, report and management)
   and redirects (comment/reply > db-page)
 * update roles when updating own comment/reply
2025-09-25 15:56:47 +02:00
Sarjuuk
12ef04c634 Template/Update (Part 14)
* convert ajax for site features
   (Profiler exclusions, favorites, custom weights, settings-cookie, contact)
2025-09-25 15:56:47 +02:00
Sarjuuk
d71ab58855 Template/Update (Part 13)
* convert dbtype 'enchantment'
2025-09-25 15:56:47 +02:00
Sarjuuk
2899cc881b Template/Update (Part 12)
* convert user page
 * update db to handle custom avatars
2025-09-25 15:56:42 +02:00
Sarjuuk
b3b790d424 Template/Update (Part 11)
* convert signin/signout functionality
 * implement 'log out all devices' option
2025-09-25 15:55:38 +02:00
Sarjuuk
b3ea80c6cc Template/Update (Part 10)
* convert language switcher
2025-09-25 15:55:38 +02:00
Sarjuuk
d03f482864 Template/Update (Part 9)
* convert filter handler
2025-09-25 15:55:38 +02:00
Sarjuuk
e17cbfe51f Template/Update (Part 8)
* convert maps tool
2025-09-25 15:55:38 +02:00
Sarjuuk
2c1b1196a7 Template/Update (Part 7)
* convert item comparison tool
2025-09-25 15:55:38 +02:00
Sarjuuk
d5275b1bf8 Template/Update (Part 6)
* convert talent calculators
2025-09-25 15:55:38 +02:00
Sarjuuk
24cb218060 Template/Update (Part 5)
* convert data loader
2025-09-25 15:55:37 +02:00
Sarjuuk
1f5152c871 Template/Update (Part 4)
* convert search into separate endpoints
 * move shared functionalty to components
 * NOTE: acceptance of opensearch has waned over the last decade and
         the script should be updated
2025-09-25 15:55:37 +02:00
Sarjuuk
81d9248541 Template/Update (Part 3)
* convert amalgamation more.php into separate endpoints
 * fix url of help articles
2025-09-25 15:55:31 +02:00
Sarjuuk
5713834f90 Template/Update (Part 2)
* convert landing (home) page
2025-09-25 15:32:59 +02:00
Sarjuuk
e943e27b5b Template/Update (Part 1)
* update TrinityCore components to return new Frontend objects
   - SmartAI => Markup
   - Conditions => Data Listview
  * update template files to accept the new Frontend objects
2025-09-25 15:32:59 +02:00
Sarjuuk
226f521439 Template/Endpoints (Base)
* redo page render following the logic of:
      Response ─┬─> TextResponse ─> TextResponseImpl
                └─> TemplateResponse ─> TemplateResponseImpl
    * split up giant files, one per response path
    * caching becomes a trait, implemented where necessary
        * TextResponses (Ajax) can now be cached
    * make use of previously defined php classes for js objects
        * Tabs, Listview, Tooltip, Announcement, Markup, Book, ...
    * \Aowow\Template\PageTemplate is the new class to be cached
    * do not discard error messages generated after vars have been sent to template
      and store in session for display at a later time
    * implement tracking consent management
    * move logic out of template into their respective endpoints
2025-09-25 15:32:18 +02:00
Sarjuuk
aeb84327d6 Template/Endpoints (Prep)
* modernize DB-Types
   - long term: should be split in class that describes the DB-Type and container class that handles multiples
 * make unchanging filter props static, allow lookup of criteria indizes through filter
 * move username/mail/password checks to util and make them usable as input filter
2025-09-25 15:32:16 +02:00
Sarjuuk
8cf0b6243d [Live] Markup/Fixup
* fix replacing tags with names
2025-09-25 15:32:14 +02:00
Sarjuuk
8243be8d8e Spells/Tooltips
* allow spells to scale up to its maxLevel instead of max player level
   (e.g. spell 42891)
2025-09-18 20:20:25 +02:00
Sarjuuk
a4a3876cdc Items/Fixup
* casting the icon string to int may be considered suboptimal
 * fixes eb3b4ca5ec
2025-08-11 23:57:15 +02:00
Sarjuuk
4c89c9061e Filter/Fixup
* lost changes to spells from 16eabb90b6
 * fix criteria access for icons
2025-08-11 19:49:48 +02:00
Sarjuuk
7d8ffdd7da ItemFilter/Fixup
* fix slot panel after 16eabb90b6
2025-08-08 22:22:38 +02:00
Sarjuuk
eb3b4ca5ec Core/Optimization
* avoid using expensive numeric cast for anything but user inputs
   especially within nested loops
 * CharStats aggregation should be about x5 faster
2025-08-08 22:10:55 +02:00
Sarjuuk
0753bfbcf6 ItemFilter/Fixup 16eabb90b6
* fix lost item grouping
 * fix type selector in filter form
 * fix item upgrade search
2025-08-06 14:19:17 +02:00
Sarjuuk
8d7c95378c Core/Cleanup
* set type declarations in DB Wrapper
2025-08-06 01:10:24 +02:00
Sarjuuk
16eabb90b6 Core/Cleanup
* move DBType Filter base to its own file under components
 * modernize class and its children
2025-08-05 21:12:23 +02:00
Sarjuuk
08f0ae711e Misc/Fixup
* fix fetching areatriggers from DB and calculating found matches
 * fix Lang concatenating an array of strings with len < 2
 * don't show debug-id col in picker windows (Summary/Profiler) and sort them by score if able
2025-08-05 21:11:02 +02:00
Sarjuuk
569c9efca4 Setup/Source
* fix copy/paste error breaking item - pvp source query
2025-07-31 18:02:32 +02:00
Sarjuuk
6d3b3e1fcb Misc/Cleanup
* modernize extAuth template and add more help text
 * type declarations + cleanup in kernel
 * respect max col size when logging errors
2025-07-28 19:47:31 +02:00
Sarjuuk
112acb2216 Zones/QuickFacts
* unlink quick facts from articles and store per-row
 * new system allows generic and manual QuickFacts to coexist
 * fill new table with data for zones
 * if someone used static quickFacts .. uh .. good luck?
2025-07-28 00:30:04 +02:00
Sarjuuk
ceec228718 Util/Mails
* make sendMail a member of Util
 * move mail templates from strings to template files
 * enabled debug output to page
2025-07-27 18:37:40 +02:00
Sarjuuk
40b5c992e2 Misc/Fixup
* make time formatting js compatible
 * add auto-discovery for rss feeds
 * fix typos
 * define more magic numbers
2025-07-27 16:42:15 +02:00
Sarjuuk
b35ab67360 Items/Stats
* fixed stats for random enchanted items with scaling enchantments
 * don't send unused 'chance' to Summary tool
2025-07-27 16:42:14 +02:00
Sarjuuk
a99fff46aa User/Sessions
* implement tracking
    * FUTURE: log out all devices for user
    * generally store less info in _SESSION
2025-07-27 16:42:13 +02:00
Sarjuuk
086760b9b1 User/Cleanup
* the great unfuckening of user and displayName
    * `login` is purely used as login with AUTH_MODE_SELF
    * `email` may now also be used to log in (if the system knows it)
    * `username` is purely used for display around the site, and lookups from web context
    * both must exist because of external logins
        a) that may be not unique
        b) you may not want to share with the rest of the world
    * todo: implement rename ( because of b) )
2025-07-27 16:42:13 +02:00
Sarjuuk
bffdb9672e Future/Frontend
* create php classes, each mirroring a js object
 * each frontend class implements __toString and json_serialize and as such can be directly used by the template
 * also allows for sane object creation before js screams in agony

 * usage TBD
2025-07-27 16:42:12 +02:00
Sarjuuk
58412e0491 Titles/Name
* partially revert 398b93e9a7
 * generic "name" is required by CommunityContent system
2025-07-27 16:42:12 +02:00
Sarjuuk
5de9759b90 DBType
* extend functions
   * FilterFactory
   * test ::hasIcon()
   * test ::isRandomSearchable()
2025-07-27 16:42:12 +02:00
Sarjuuk
967841fcb9 Spell/DetailPage
* finally set GCDCat var
 * (a smooth decade later and it turns out it was StartRecoveryCategory all along)
2025-07-27 16:42:12 +02:00
Sarjuuk
3f0d6c2de6 JS
* add clickToCopy functionality
2025-07-26 23:27:11 +02:00
Sarjuuk
0928b1b430 User
* slightly modernize static class
2025-07-26 23:18:59 +02:00
Sarjuuk
0562989196 Fix LocString serialization
.. just implement __serialize, d'uh!
2025-07-26 23:16:07 +02:00
Sarjuuk
06bd7aa665 More/Searchbox
* modernize article
 * (clicktocopy pending cherry-pick)
2025-07-26 23:04:23 +02:00
Sarjuuk
a7cf96307c JS/CSS
* remove vendor-specific styles and replace with generics where necessary
 * remove browser hacks as far as possible (Presto/Trident)
 * remove FontAwesome reference
 * minor cleanup
2025-07-26 20:36:01 +02:00
Sarjuuk
8403f9ffd9 Comments/Replies
* fix load on demand for more than 5 replies per comment
2025-07-26 20:19:21 +02:00
Sarjuuk
fbfb81cd25 Profiler/Talents
* use icon from g_file_specs instead of weightscale data
 * fix detecting tank subspec for class 6
2025-07-24 19:43:09 +02:00
Sarjuuk
7a803a8783 Profiler/Talents
* fix grabbing unused gyph items for spec (like 44432)
2025-07-24 19:43:08 +02:00
Sarjuuk
dfefa914af Misc/Fixup
* remove vintage copy-paste error (that probably didn't do anything) in SmartAI
 * fix well-riped copy-paste error (that definitely did something) in Conditions
2025-07-18 02:29:20 +02:00
Sarjuuk
707dc32495 Util/Sanity
* break closing html tags contained in js
2025-07-07 17:55:11 +02:00
smansesdottk
e7baa27e27 Misc/Typos
* fix typos in README.md
 * cherry-picked from smansesdottk/www-aowow
2025-07-05 20:53:15 +02:00
Sarjuuk
f826e4d68a NPCs/Vendors
* fix npc_vendor self-referencing itself
 * game_event_npc_vendor seems to not allow for self references
2025-07-04 22:29:15 +02:00
Sarjuuk
e173de9a97 Quests/Series
* apply cuFlags to quest series
 * implement firstquestseries, lastquestseries, partseries filters using said flags
2025-06-22 19:31:22 +02:00
Sarjuuk
7c527e6144 Quest/Series
* fix series steps not displaying quests running in parallel for this step
2025-06-20 19:04:22 +02:00
Sarjuuk
74c0727bdb Profiler/ArenaTeams
* filter unused teams explicitly by played game count instead of implicitly by rating
2025-06-17 18:02:56 +02:00
Sarjuuk
cd94a2fa4e Profiler/Misc
* fix realm selection in profiler filters
 * minor cleanup of prQueue
2025-06-17 17:45:49 +02:00
Sarjuuk
91bb53aa1d Profiler/Realms
* skip out of List construction if realms are empty or do not match preselection
 * also prefilter Guilds and Prolfiles Lists for server or region (unless custom profiles)
 * discard ProfileList entries for inaccessible realms
2025-06-13 21:04:32 +02:00
Sarjuuk
069ca27b35 Localization/esES
* having ChatGPT translate missing strings
    * cherry picked examples seem fine, even those with control sequences
2025-06-03 20:50:13 +02:00
Sarjuuk
c3048fe1f8 Misc/Fixup
* test arrays first before use, not the other way round
 * do not try to init local arena team entries if there are no entries in list.
 * fix equally distributing chars/guilds/arenateams across realms for unlimited (0) lists
 * fix double declaration of realms in ArenateamList
2025-06-03 17:13:44 +02:00
Sarjuuk
ee02e70571 Profiler/Sync
* only truncate arena team members for OTHER teams of the given size
2025-06-03 17:02:24 +02:00
Sarjuuk
af69aa8e94 Profiler/ArenaTeams
* fixed LocalArenaTeamList being incompatible with it's inherited getListviewData()
 * also apply region and server limiters in LocalArenaTeamList
2025-06-03 17:02:23 +02:00
Sarjuuk
1de0535629 Mapper/Fixup
* no longer need to provide empty mapperData object on ZoneListPage
2025-05-23 19:59:51 +02:00
Sarjuuk
9b6ed672c4 Misc/Fixup
* FCK BOM
 * ItemsetDetailPage is visible again
2025-05-23 19:04:49 +02:00
Sarjuuk
9d704c5ff9 ItemDetailPage/Vendors
* show mapper for vendor locations
2025-05-20 20:07:32 +02:00
Sarjuuk
6475a9d181 SpellDetailPage/SpellGroups
* get a spells first rank from TC table if able
 * fix display of spell group stack rules tab
2025-05-17 22:01:21 +02:00
Sarjuuk
a8e1e3cf19 Misc/Fixup
* fixed typo when trying to include file for CharStats handling
2025-05-14 21:53:19 +02:00
Sarjuuk
44e0b6c62d Spells/Racial Skills
* fix manually set race mask for expansion races (and one class mask)
 * show applicable classes in race detail page listing
2025-05-13 20:11:20 +02:00
Sarjuuk
b837e55edc Misc/Fixups
* copy/paste errors in MiscValue handling for SpellEffect: skinning
2025-04-23 01:28:53 +02:00
Sarjuuk
d75aa56b38 Spells/Attributes
* update spell attribute descriptions primary from TC, secondary from Wowdev Wiki (not guaranteed to be correct for 335)
 * show all attributes on SpellDetailPage
 * update links from attributes to SpellFilter
   * unsure: should the attribute filters work purely on attributes or also consider other factors?
 * implement some of the client side modifiers on the spell tooltips and buffs
2025-04-22 21:38:58 +02:00
Sarjuuk
9c73f3cf95 Style/Fixup
* vertical align tinyicons in SpellMod array on SpellDetailPage
2025-04-13 00:37:36 +02:00
Sarjuuk
9a893e03d7 Misc/External Links
* try to prompt the brwoser to not send a referrer header when leaving the site.
2025-04-11 04:52:22 +02:00
Sarjuuk
43970189a6 Misc/Fixup
* happy three quarters of a new year .. or something
2025-04-02 23:34:36 +02:00
Sarjuuk
682b315e17 User/Misc
* floating changes
 * codify user checks into functions
2025-04-02 23:26:01 +02:00
Sarjuuk
3078763ec3 Misc/Fixup
* floating changes
 * condense several switch() into match() constructs
 * fix ucFirst not making the rest of the word lowercase
2025-04-02 22:22:57 +02:00
Sarjuuk
44ff43c113 Misc/Fixup
* floating changes against account ajax
 * make validation of DBtype/typeIds a method of Type
2025-04-02 22:14:45 +02:00
Sarjuuk
790264ba08 Misc/Fixup
* floating changes against profiler
 * no functional changes
2025-04-02 21:49:25 +02:00
Sarjuuk
e29d1e69fe Template/Fixup
* those js snippets don't really care what namespace they are included in
 * .. why are they even php files...?
2025-04-01 23:18:24 +02:00
Sarjuuk
2689fba992 SpellDetailPage/Source
* do not acces sources directly
 * fixes source display in infobox
2025-04-01 23:10:24 +02:00
Sarjuuk
db1d3ccace Core/Cleanup
* try to give included files a logical structure
 * move objects from Util and Game to their own files
 * make non-essential files auto-loaded
2025-04-01 22:33:36 +02:00
Sarjuuk
3a6c86092b Core/Compat
* create namespace Aowow to avoid naming conflicts
 * inclues/libs/ is outside of the Aowow namespace
2025-04-01 22:32:37 +02:00
Sarjuuk
4ccf917707 Listview/ReplyPreview
* fix links to parent comment
 * implement go-to-reply redirect
2025-03-29 14:15:59 +01:00
Sarjuuk
b347794ce5 Guides/Editor
* fix language selector
 * fix changelog
2025-03-29 13:52:36 +01:00
Sarjuuk
ed25f1f5f5 Misc/Fixup
* add type guide (300) to js so comments can be linked to guides
2025-03-26 21:22:37 +01:00
Sarjuuk
390801f53c NpcDetailPage/Skinning Skill
* fix displayed skill requirement for non-standard skinning (engineering, herbalism)
2025-03-25 17:30:33 +01:00
Sarjuuk
73f4a69a41 Setup/CustomData
* fix entry for item 33147
2025-03-22 19:24:01 +01:00
Sarjuuk
197b097ee5 Filters/StaffFlags
* fixed flag filters visibility for spells
 * fixed implementation of staff flag filters. Now they acutually do something.
 * fixed localization
2025-03-20 21:30:01 +01:00
Sarjuuk
f2bbb87eef SpellDetailPage/Effects
* readd lost markup used by EFFECT_PLAY_SOUND, EFFECT_PLAY_MUSIC and AURA_OVERRIDE_SPELLS, AURA_SCREEN_EFFECT
2025-03-20 19:07:21 +01:00
Sarjuuk
676c8617a5 Misc/Fixup
* fixed an exception for using an enum as string when generating log message.
2025-03-19 13:50:28 +01:00
Sarjuuk
6f8b72c980 Spells/Tooltips
* use cooldown strings from future clients to avoid scientific notation for long cooldowns hidden by the 335 client
2025-03-16 01:11:32 +01:00
Sarjuuk
b2d3bc1076 Types/Filters
* followup on 748a78c3c7
 * fix return type of criteria filter callbacks (mostly in case of faulty input)
 * spread the criteria var into criteria filter callbacks
 * some magic numbers to constants and type declarations for params/return types
2025-03-12 00:29:23 +01:00
Sarjuuk
9345309c07 ItemDetailPage/Tabs
* add filter result prompt to "disenchanted from" tab
2025-03-11 22:16:43 +01:00
Sarjuuk
50a5ccbaf6 Items/Summary
* Fixed itemset bonus calculation for sets with interchangeable items.
 * Fixed multiple items for the same slot activating set boni
2025-03-07 16:29:14 +01:00
Sarjuuk
460f4857ad Items/Filters
* add 'fished' as valid item source
 * item source 'none' now properly selects items without source, not items without source from this list
 * fixed edgecases where filter callback might have no return value
2025-03-06 18:51:51 +01:00
Sarjuuk
3b0d288418 Profiler/Fixup
* profiler expects teamId, not sideId
   (yes i know they are basically the same thing offset by 1!)
2025-03-06 02:00:28 +01:00
Sarjuuk
c712d1234b WorldEvents/Query
* try to fix generated aggregate query by not selecting multiple cols as `id`
2025-03-04 23:49:32 +01:00
Sarjuuk
31ad2e4944 Misc/Locale
* coerce malformed locale values from GET to int instead of failing outright
2025-03-04 23:49:27 +01:00
Sarjuuk
6249f2957e User/Fixup
* user linked characters where also uninitialized
2025-03-04 23:49:15 +01:00
Sarjuuk
efc8b51c8f Enchantment/Spells
*  initialize SpellList as null so it can be referenced
2025-03-04 00:29:08 +01:00
Sarjuuk
44bd9f521b Misc/Fixup
* fixed return type declaration to match inherited class
2025-03-04 00:22:13 +01:00
Sarjuuk
8b59551905 Locale/Search
* remove obsolete and broken logographic check.
 * this fixes searches with "too short" search tokens
2025-03-03 19:42:00 +01:00
Sarjuuk
4fe930cdca Quest/Condition
* fix quest display for reputation restricted quests
 * also check sources from externaly added conditions
2025-03-01 22:00:12 +01:00
Sarjuuk
337eddcc0b Fixup/Spells
* fixed variable reuse
2025-03-01 21:17:34 +01:00
Sarjuuk
013f1845b5 Fixup/Class/Race
* matching bitmasks on IDs may be considered counterproductive
2025-02-28 19:17:17 +01:00
Sarjuuk
4c6d93881c Spells/Fixup
* format effect cooldown as time
2025-02-28 14:36:49 +01:00
Sarjuuk
fa33467610 Misc/Cleanup
* more trait properties to const as they were always intended to be
2025-02-28 14:05:04 +01:00
Sarjuuk
1f59e6fe2d Util/Enums
* create helper enum classes ChrClass & ChrRace
 * replace various iterators and checks with said enums
2025-02-28 14:04:19 +01:00
Sarjuuk
a5bd6ddc8a Misc/Fixup
* fix external links by escaping quest name as javascript
2025-02-27 16:25:25 +01:00
Sarjuuk
6660125154 Items/Tooltips
* fix scaling string in item tooltips for scaling & non-interactive tooltips
2025-02-27 16:25:25 +01:00
Sarjuuk
9abe9b0d56 Loot/Conditions
* do not link questitems for multiple quests with 'and'
2025-02-27 16:25:25 +01:00
Sarjuuk
83b99c47d2 Auth/Fixup
* provide empty values for non-default db fields when initializing external accounts
 * removed browser check for username maxlength as it depends on auth method
2025-02-27 16:25:24 +01:00
Sarjuuk
cd4c023c61 SpellDetailPage/Fixup
* fix extraCols from spell_loot bleeding into other tabs
 * fix linking to effect spell from glyph_properties
2025-02-27 16:25:24 +01:00
Sarjuuk
870cbea2ca SmartAI/Update
* update events and actions to match TrinityCore again
   * removed events and actions have been kept but marked as deprecated
 * general rewording and use of UIES for better readability
 * move constants to respective classes
 * reevaluate usage of UNIT_FIELD_BYTES1 content
2025-02-27 16:25:24 +01:00
Sarjuuk
748a78c3c7 Filters/Cleanup
* move filter constants to class Filter
 * use less magic numbers
 * add type declarations
2025-02-27 16:25:23 +01:00
Sarjuuk
398b93e9a7 Locale
* detatch from User and Util and move to its own enum class
 * added definitions for all locales the 12340 client could in theory have
 * this is incompatble with the Intl extension
 * version bump and php requirement bump
2025-02-27 16:25:23 +01:00
Sarjuuk
40c2c63d1b NPC/MapModes
* try to improve map mode detection for NPCs again...
2025-01-27 23:09:45 +01:00
Sarjuuk
9da1e1575f Locale/Typo
* fixed title categories for locale 8
 * .. after a decade
2025-01-27 10:31:23 +01:00
Sarjuuk
88c066a8f5 ErrorHandling/Fixup
* do not write to STDERR from web context
2025-01-27 10:31:23 +01:00
Sarjuuk
88da3588e5 Compat/SQL
* make ON DUPLICATE KEY UPDATE queries compatible with both MySQL8 and MariaDB by providing update values from php
2025-01-26 13:53:26 +01:00
Sarjuuk
5309843d77 Misc/Robots
* add sensible robots.txt expected by configuration self test
2025-01-20 07:09:48 +01:00
Sarjuuk
452e056499 Misc/Date Format
* use 12h format when using AM/PM suffix
2025-01-20 06:41:32 +01:00
Sarjuuk
d86936f6f5 Setup/ItemStats
* fixed array structure when converting equal amounts of SPELL_DAMAGE and SPELL_HEALING to SPELL_POWER
2025-01-20 06:34:38 +01:00
Sarjuuk
c1eecb4c22 ItemStats/Filters
* do not use NULL on item stats as it prevents searching for an amount of 0 (except for stats that certain items just cant have)
 * fix stats from spells granting spell power and spell healing separately
 * define and use some item subclasses
2024-09-10 16:54:49 +02:00
Sarjuuk
a62f24b97c Locales/Domain
* entirely switch over to write 'en' for locale 0 and accept both 'www' and 'en' for locale 0 when receiving
2024-09-10 14:42:27 +02:00
Sarjuuk
e3fc4ebd62 Updates/Fixup
* DROP COLUMN IF EXIST is MariaDB specific and should not be used.
2024-09-08 21:46:59 +02:00
Sarjuuk
79aa8fda7e Profiler/Fixup
* fix index error in ArenaTeam member update query
2024-08-27 22:56:33 +02:00
Sarjuuk
fdf8d783b1 AjaxHandler/Fixup
* fix invalid return type in handleWeightscales() for action: delete
2024-08-27 22:34:52 +02:00
Sarjuuk
48ce7267e7 ItemFilter/Fixup
* fix invalid return type in createConditionsForWeights()
2024-08-27 22:34:52 +02:00
Sarjuuk
5270e70cd1 ListviewTabs/Fixup
* filter empty tabs before trying to display, case in point
 * do not always assign conditions tab if empty
2024-08-27 22:34:52 +02:00
Sarjuuk
50f5af07f3 SpellEffects/Icons
* fix icon formater: num/qty can be strings
2024-08-27 22:34:52 +02:00
Sarjuuk
64fb86f3a9 Reports/Fixup
* reports can have 'null' subject (general bug reports, general feedback)
2024-08-27 22:34:52 +02:00
Sarjuuk
778c21e817 BaseType/Fixup
* fix totals query. Rather treat original query as subquery than trying to modify it
 * fixes queries utilizing HAVING and GROUP BY
2024-08-27 20:46:06 +02:00
Sarjuuk
af303f447a CommunityContent/Fixup
* fix totals query params after 481a3dc63f
2024-08-27 20:44:43 +02:00
Sarjuuk
03cf3a5918 UtilityPage/Random
* fix invalid type error
 * simplyfy query building (there are no longer tables with an index named `entry`)
2024-08-27 20:42:06 +02:00
Sarjuuk
3aede18926 SpellDetailPage/Fixup
* fix empty scaling bar
 * fix setting max level for scaling tooltips
 * fix displayed periodic power gain value for runic power and rage
 * attempt to fix spell coefficient calculation (dear god...)
 * append % sign to value for two more auras
 * display value per combo point
2024-08-24 20:02:51 +02:00
Sarjuuk
075e15ba0c Setup/Fixup
* even more forgotten renames in help text
 * see 07e001ee9c
2024-08-24 15:11:31 +02:00
Sarjuuk
7d2e306f0c Game/Spawns
* fix spawn point calculation on instance maps without dungeonmap entry
   (can't wait to see what breaks elsewhere)
2024-08-24 14:49:22 +02:00
Sarjuuk
b11c1125f6 Filters/Wildcards
* fix usage of generic search wildcards: ? *
 * only transform search form fields expected to be text
 * some cleanup (magic numbers to define, fn return types, nullsafe assignemnts)
2024-08-22 16:15:07 +02:00
Sarjuuk
57ad861da7 Items/Arena
* changed logic to not display required arena rating in tooltip or infobox if there are sources that don't require it.
 * also, should there be a difference in sources, display lowest required rating, not not highest.
2024-08-22 01:40:03 +02:00
Sarjuuk
d2e109d818 SpellDetailPage
* attempted cleanup in effect generation and template
 * display summon properties
 * display unit of effect value
 * fix spell effect layout, typos, missing tools, ...
2024-08-19 21:35:36 +02:00
Sarjuuk
dab110475c Items/Tooltips
* do not display crafted item tooltip inside recipe tooltip if the craft spell doesn't create an item.
   (notably: enchantments were displaying enchantment scrolls)
 * always display reagent cost if set
2024-07-31 22:02:15 +02:00
Sarjuuk
b330f88699 Itemstats/Ammunition
* don't use 'delay' to calc added dps from ammunition
2024-07-31 02:38:25 +02:00
Sarjuuk
481a3dc63f MySQL/Compat
* fixed several deprecation notices and warnings from MySQL8, most notably:
   - SQL_CALC_FOUND_ROWS: stopped using DBSimple::selectPage and query 'SELECT COUNT(*) ...' separately where needed
   - ON DUPLICATE KEY UPDATE ... VALUES(): use row alias for new values instead of VALUES function
   - boolean shorthands to long form (&& -> AND, etc)
2024-07-31 02:38:19 +02:00
Sarjuuk
1b2b773663 PHP/Compat
* avoid using "echo" to write to CLI as php mistakes it for sent headers (see php-src #12303)
 * as we are using fwrite now, errors are written to STDERR instead of STDOUT
 * fixes an issue where reloading the config would cause "ini_set(): Session ini settings cannot be changed after headers have already been sent" to be spammed
2024-07-31 00:51:10 +02:00
Sarjuuk
b5c2f7a296 Git/Misc
* force text files eol to \n
2024-07-31 00:50:46 +02:00
Sarjuuk
07e001ee9c Setup/Fixup
* fixed help text referencing renamed parameter
2024-07-31 00:50:46 +02:00
Sarjuuk
c30e68d86f Enchantments/ProcSpells
* fixed resolving procSpells with a triggerSpell of 0
 * spells are now always displayed in listview
2024-07-14 17:48:30 +02:00
Sarjuuk
7d545167df Errors/Log
* also log POST data
2024-07-14 17:18:22 +02:00
Sarjuuk
31928d56a7 Localization/Fixup
* kill an antique typo causing an error whenever a tooltip for a hearthstoneesque effect is displayed for Locale FR an RU
2024-07-14 16:23:43 +02:00
Sarjuuk
99a95f3995 Profiler/Completions
* removed old workaround for js date object creation
2024-07-13 19:22:25 +02:00
Sarjuuk
a9f1832b6d GearScore/Enchants
* reapply lost /4 modifier for enchantment scores
2024-07-11 01:48:20 +02:00
Sarjuuk
b0a51f4746 Spells/SpellClassMask
* fix confusion of SpellClassMask fields
 * no functional change
2024-07-10 18:04:35 +02:00
Sarjuuk
f55945780b Setup/Fixup
* fix syntax of db_structure.sql
 * not to self: SHOW CREATE TABLE doesn't return a terminated statement
 * fixes #431
2024-07-09 21:10:17 +02:00
Sarjuuk
81078bcf3d Comments/Date
* add missing elapsed value to comment and reply rows to make them sortable
2024-07-09 14:41:05 +02:00
Sarjuuk
c14d53067b Comments/Replies
* add default values to comments table so replying doesn't cause errors
2024-07-09 13:30:51 +02:00
Sarjuuk
460615c112 Profiler/Completion
* split completion table into it's subcomponents
 * this should save some disk space as some keys and null fields have been optimized out and col sizes have been reduced
 * sort ICC raid bosses first to last
2024-07-08 23:56:43 +02:00
Sarjuuk
98b1771850 Config/Locales
* flag locales as 'required'
 * allow bitmasks to be empty unless required
 * skip empty check in javascript
2024-07-08 17:16:10 +02:00
Sarjuuk
37def70f6a Setup/Realms
* force use of Locale EN instead of last used locale (usually RU)
2024-07-08 16:51:44 +02:00
Sarjuuk
f4364099c6 DB/CustomData
* alter value column from varchar to text to prevent truncated data
2024-07-08 16:51:44 +02:00
Sarjuuk
92a6e0122f Misc/Fixup
* Profiler: handle items with invalid enchantments
 * Profiler: convert (last?) forgotten INSERT IGNORE to ON DUPLICATE KEY UPDATE id=id
 * Areatrigger: fix null related tabs
 * LatestComments: fix lv template for replies
 * Conditions: fixed tab names for locale DE
2024-07-08 16:51:08 +02:00
Sarjuuk
828fa40d4b Loot/LootLink
* link Ahune and Majordomo Executus to their chests
2024-07-07 23:02:44 +02:00
Sarjuuk
815f13e530 Pages/TopUsers
* hide 'reports' column as reputation for reports is NYI
2024-07-07 20:55:00 +02:00
Sarjuuk
9435c0fc2e EventDetailPage/RelItems
* fixed getting related items from QuestList
2024-07-07 20:50:22 +02:00
Sarjuuk
b8898797ed SoundDetailPage/Conditions
* fix error assigning a worldstate conditional from child zone to parent zone
2024-07-07 14:18:08 +02:00
Sarjuuk
be7a84a651 Core/Conditions
* minor rework
 * fixed columns of tab item loot
 * fixed lookup of classes/races (as they are the only type used as bitmask)
 * implemented reverse lookups everywhere (arguably class, race and skill are too spammy)
 * reverse lookups no longer contain redundant data
 * changed how the groupKey is set, so there are no more cases that can't be looked up
 * fixes #273

 * title: added tab 'criteria-of'
2024-07-06 04:03:56 +02:00
Sarjuuk
0f6b8015a1 Misc/Fixup
* Loc EN: removed unnessecary 'to translate' backets
 * npc: add missing index to loot tabs to kill a warning
2024-07-05 22:10:46 +02:00
Sarjuuk
bd5200de85 Mapper/Quests
* sort zones by number of pins, most to least
 * always preselect zone with most pins
2024-07-05 22:10:36 +02:00
Sarjuuk
f21e8045b2 Misc/Fixup
* spell:    resolve MiscValue for SPELL_AURA_MOD_INVISIBILITY
 * search:   do not double escape page title
 * profiler: fixed typo, causing shaman spirit weapons to be applied _everywhere_
2024-07-05 18:15:05 +02:00
Sarjuuk
8d885a5a67 Setup/WIN
* fix paths for WIN ... and try to not use slashes in the future...
 * fixes #430
2024-07-05 18:15:05 +02:00
Sarjuuk
a4bcb33ba4 NPCs/DetailPage
* added item to infobox when npc is a spirit (only visible when dead)
2024-07-05 18:01:49 +02:00
Sarjuuk
8b46607c29 Setup/Spawns
* handle erronous zone data from TDB creature/gameobject tables
2024-07-04 23:22:29 +02:00
Sarjuuk
c3bae7fe5e Localization/zhCN
* add lost string lookups
2024-07-04 21:31:52 +02:00
Sarjuuk
2e9b503c59 Loot/LinkedLoot
* rework npc <-> chest loot linking
 * difficulty is now directly stored
 * should fix multiple issues where loot tabs had wrong difficulty or null title
2024-07-04 21:31:48 +02:00
Sarjuuk
02e33b4038 Profiler/Tooltips
* do not transform page parameters to lowercase. The tooltip Javascript expects the server response in the same case it got sent.
 * transform to lowercase just for lookups
 * fixes #394
2024-07-04 16:27:00 +02:00
Sarjuuk
c3347b8e9c CLI/WIN
* read input as a whole line under WIN instead as stream_get_contents always blocks
 * fixes #429
2024-07-04 15:20:07 +02:00
Sarjuuk
cd4e049680 DB/Engine
* drop usage of MyISAM and switch to InnoDB.
2024-07-03 18:45:52 +02:00
Sarjuuk
2bd588045a PHP/Compat
* remove sources of deprecation warnings. Mostly dynamic creation of object properties.
 * some string function no longer accept null as string
2024-07-03 18:38:28 +02:00
Sarjuuk
05c036bd9f Misc/Fixup
* fix Conditions derived from spell_area
 * Type Statistic should not be random searchable and excluded from "missing screenshots" UtilityPage
 * do not cut off querys in log
2024-07-03 18:33:25 +02:00
Sarjuuk
d93b5df5bc Misc/Fixup
* added datetime increment to dbversion table, lost from 69de457108
2024-07-02 16:12:13 +02:00
Sarjuuk
f12d16ea5b Localiziation/Fixup
* fixed cast time strings for locale enUS
2024-07-01 22:25:06 +02:00
Sarjuuk
d2277d1034 Setup/Icons
* do not consider the setup step failed if icons are missing. There are a couple void references in dbc
2024-07-01 19:44:31 +02:00
Sarjuuk
4d306e64fb CLI/Config
* fixed skipping the value in oneline mode being interpreted as empty string
 * fixed entering infinite loop in oneline mode, when passed value was invalid
 * fixed displaying config hint, when passed value was valid
2024-07-01 19:05:33 +02:00
Sarjuuk
8016802ec6 CLISetup/Fixup
* also convert Cfg test function, forgotten in 2386e35207
2024-06-30 16:55:22 +02:00
Sarjuuk
2386e35207 CLISetup/locales
* removed redundant declaration of locales to process
2024-06-29 13:03:18 +02:00
Sarjuuk
5d4051928a Setup/Fixup
* spell:  do not create a temporary copy of the spell.dbc, but merge serverside spells directly into aowow_spell.
           This sidesteps a mysql issue where a temp table can't be read multiple times.
 * spawns: fixed lost var rename in log output
 * items:  use UNSIGNED instead of INT when typecasting in query so mysql can also understand it
2024-06-29 12:18:13 +02:00
Sarjuuk
33d2192431 JS/Mapper
* generally allow zone links on a map on the zone detail page
 * manually disable it for the main map
2024-06-28 01:04:48 +02:00
Sarjuuk
ae54e5e213 Setup/Spawns
* restore info on manually moved spawn points
 * don't use worldmaparea zone dimensions for maps that don't use them
2024-06-28 01:04:42 +02:00
Sarjuuk
cdf06deb90 Misc/Fixup
* restore display of creature targets on quest detail page
 * fix ShowOnMap utility for quest displays
2024-06-28 01:04:36 +02:00
Sarjuuk
69de457108 Map/Spawns
* move areatrigger teleport endpoints and instance entrance points to spawns. This makes them editable like spawn points.
 * since instance entrances aren't shown on maps they are moved through the admin menu on the instances page.
 * rewrote spawns SetupScript. Individual groups can now be recalculated separately.
2024-06-28 01:04:29 +02:00
Sarjuuk
040cac41a1 Setup/Help
* created a generic help display for SetupScripts with sub commands
2024-06-28 01:04:24 +02:00
Sarjuuk
abbedf9ae4 Misc/README
* extracting Glues/Credits/ is optional. They are not used by aowow itself.
2024-06-27 14:51:16 +02:00
Sarjuuk
22b0f8c1c1 Setup/Update
* setup step names are allowed to have dashes and underscores
2024-06-26 16:52:45 +02:00
Sarjuuk
a4c734435e Map/ShowOnMap
* fix pins being combined across multiple floors
2024-06-26 16:52:45 +02:00
Sarjuuk
88c5127ab5 Map/Spawns
* fix spawns for multifloor dungeonmaps that use their worldmaparea entry for coordinates
 * fix mapper feature: move visible spawn point
 * resolve confused X/Y coordinate remapping from a time before i knew world coordinated are rotated by 90°
 * PS: Ulduar floor indizes were funky. No idea why.
 * restore old define order for g_zone_areas
2024-06-26 16:52:00 +02:00
Sarjuuk
a08a713dcc Misc/Fixup
* fixed typo in locale deDE
 * fixed markup not processing emotes with negative id
2024-06-26 16:42:04 +02:00
Sarjuuk
79764ced60 Core/ErrorHandler
* do not handle errors outside of the registered handlers
 * always handle all errors otherwise they get stored for error_get_last
 * always print errors to CLI
 * shutdown function handler should not be picky about what errors it gets to report
 * removed some mostly unused error strings
2024-06-24 18:10:44 +02:00
Sarjuuk
e614f415a9 Util/FileHandler
* try to create directory if file is to be written into nonexistent directory
2024-06-24 17:13:50 +02:00
Sarjuuk
10de320616 CLI/readline
* added checks if STDIN is available and open before accessing it.
2024-06-23 15:54:58 +02:00
Sarjuuk
9bb5afd9a7 Setup/CustomData
* removed unused option to have custom data defined in code
 * table `aowow_setup_custom_data` should be used instead
 * this kills a "creation of dynamic property" deprecation notice
2024-06-23 14:42:16 +02:00
Sarjuuk
615c203c7a Setup/img-maps
* script cleanup
 * fixed subzone generation and made color less garish
 * fixed alphamap generation and alphamapcheck not pointing to the same path
 * fixed padding UtilityScript args with unexpected var types
2024-06-21 16:57:33 +02:00
Sarjuuk
931a90f5c8 Misc/Fixup
* revision bump
 * minor log format fix
2024-06-20 20:49:11 +02:00
Sarjuuk
bf184e7555 Core/Setup
* rewritten to be able to dynamicly load it's components
   - CLISetup -> checks for UtilityScripts (config, setup, dbc reader, etc.) -> checks for SetupScripts (individual sql/file generators)
   - each step may now have a help prompt attached. If none are provided, the containing script may provide it's help.
   - all Scripts are self contained modules. No more editing of 3+ files if some component is added/removed
 * removed intermediaries FileGen & SqlGen
 * functional changes
   - allow providing CLI arguments to siteconfig and account UtilityScript and skip the interactive prompts
   - set slot for consumable enchantment items so they are filtrable
   - zones dataset is now localized and generated from GlobalStrings.lua and DungeonMap.dbc. Related data dumps removed.
   - 'aowow' and 'prQueue' executables now have shebangs

    WARNING - command line options have been renamed!
2024-06-20 18:10:12 +02:00
Sarjuuk
ab4cf67e80 Misc/Fixups
* Util::writeTable .. border length, toned down colors
 * Cfg::get .. empty strings and 0 can be valid, return null on failure
 * Game::getWorldPosForGUID .. fixed referencing soundemitters by soundId instead if index
2024-06-20 16:28:29 +02:00
Sarjuuk
7412a518a5 Setup/Classes
* fix skill aggregate
 * ya cant sum strings, ya dummy
2024-06-19 02:52:43 +02:00
Sarjuuk
3d84870db8 DBC/Fixup
* fix broken locale string extraction after e164023b8a
2024-06-19 01:34:35 +02:00
Sarjuuk
b5b62a5a62 Localization/GlobalStrings
* apply more strings from GLobalStrings.lua to localization
 * mostly ITEM_MOD_* and PVP_RANK_*
 * some fixes in the process
2024-06-18 22:59:20 +02:00
Sarjuuk
7e0be11323 Stats/CombatRatings
* define magic numbers from combat ratings
 * move forgotten rating base values from Util to Stats
 * fix localized rating strings for item and spell tooltips (expected float, got string)
 * some cleanup in item/spell types to make it more clear when an internal stat is being handled, not an itemMod or combatRating
2024-06-18 22:34:50 +02:00
Sarjuuk
e164023b8a Setup/DBC
* move dbc structures to separate files and allow loading a specific build
 * handle localized single string fields
 * add cli option for output table name
 * add cli option for wowbuild
2024-06-17 19:59:51 +02:00
Sarjuuk
12ddc6fe82 Misc/Fixup
* prevent direct access to some internal files where applicable and reword error for others
 * if CFG_DEBUG is set also enable debug in client javascript
 * non-breaking formatting changes
 * define regions and missing locales for later use
2024-06-17 19:59:51 +02:00
Sarjuuk
06ffba0239 CLI/readline
* do not reuse prompt variable for use input
2024-06-17 19:59:51 +02:00
Sarjuuk
d03448b053 CLI/FileAccess
* fix deep file/dir creation
 * also edited file permissions again and left a note so it doen't happen again .. maybe
2024-06-17 18:13:03 +02:00
Sarjuuk
0117916da9 CLI
* make tables more legible
2024-06-17 18:13:03 +02:00
Sarjuuk
c2bbfe17a6 Setup/DataStores
* do not rely on temporary converted dbc tables during runtime
 * create permanent tables instead
2024-06-17 18:12:59 +02:00
Sarjuuk
7b924a197e Mapper/Spawns
* do not try to place spawns assigned by instanced map <=> zoneId association. They have (0,0) as coordinates
2024-06-17 13:36:45 +02:00
Sarjuuk
8fe18ed41c Articles/Classes
* add class articles from 8e5bdebea0 db_structure.sql
2024-06-16 22:16:38 +02:00
Sarjuuk
d16b08bb29 Core/CharStats
* unify stat handling. If there are discrepancies left at least they are now centralized.
   - health should no longer pretend to be mana
   - stats are no longer capped to 32.7k points as items can have multiple of the same stat and handily exceed smallints
 * weight scale fixes:
   - "repair cost" is no longer a weight scale option. Why was it one in the first place? Also it wasn't even accessible before. (that was a bug)
   - "bonus armor" is now searchable and only applied to armor pieces
 * removed unused parsed stats from itemsets
2024-06-16 19:28:26 +02:00
Sarjuuk
cb3c7d4ef0 Misc/Util
* relax numeric type requirements when working with filters
 * restore smart type casting functionality of Util::checkNumeric when used with NUM_ANY
 * enable Util::arraySumByKey to work recursively
 * fix source display in listview
2024-06-16 17:22:08 +02:00
AthenaSui
a4d05dc036 Localization/zhCN
* cherry-picked translation from 820c88d412
2024-06-14 02:01:54 +02:00
Sarjuuk
ce0e57e390 Setup/Fixup
* run TDB checks agains word db as they are supposed to.
 * fixes #423
2024-06-10 21:07:57 +02:00
Sarjuuk
33ee358e0c Setup/DBconfig
* test for known world db formats and throw appropriate errors
 * test for imported aowow db and throw error if missing
 * make DB connection reloadable
2024-06-09 15:02:31 +02:00
Sarjuuk
82c04c9ed5 CLI/readline
* there are still terminals out there sending \r as line terminator...
2024-06-07 17:15:01 +02:00
Sarjuuk
fc86825b15 Pages/Item
* fixed fractional buy price for items sold in stacks in infobox (fixes #362)
 * fixed filter criteria enums being generally invalid
2024-06-07 16:40:26 +02:00
Sarjuuk
e873d8cbd0 CLI/readline
* ignore \e sequences except the single \e chars read to leave the current loop
2024-06-07 15:32:35 +02:00
Sarjuuk
69fe0b5c7d Cfg/Fixup
* also trigger first time load as soon as db is set up
2024-06-04 23:15:18 +02:00
Sarjuuk
e734b41632 Cfg/Fixup
* only throw errors if there is actually a config to work with. (like, not when you are just setting up for the first time)
 * do not use trigger_error() in CLI mode as it can cause a lockup as the error_handler tries to throw more errors.
 * assign lost validator fn flag to settings
 * also use validator onReset
2024-06-04 22:15:52 +02:00
Sarjuuk
5c1e9747c6 Misc/Cleanup
* remove some derelict code
2024-06-03 15:58:41 +02:00
Sarjuuk
f861886fdf Misc/Fixup
* Conditions: loot rows initially have no 'id'
 * fixed building talent string for hunter pets (different talents can occupy the same row/col spot)
 * added keys loot cols on creature table
 * fixed trying to show itemset type for itemsets without type
 * fixed waypoint calculation when moving entity between floors
2024-06-02 21:15:29 +02:00
Sarjuuk
efab0bad32 Misc/Fixup
* missed const from f77d676a19
 * missing start of string anchor in Util::checkNumeric
2024-06-02 02:45:29 +02:00
Sarjuuk
bc7d561da2 Core/Conditions
* rewritten and moved to its own class, should be easier to expand in the future
 * add missing sources and types from TrinityCore
 * implement conditions on Areatrigger and Loot containers
 * implement reverse lookups (e.g. a spell is a conditional for something else)
 * general beautification pass .. should be more legible in general

NOTE:
 * texts have been changed, so the existing translation for esES ist gone
 * selecting and describing condition targets is still wonky
2024-06-01 02:47:58 +02:00
Sarjuuk
84555afae3 Page/Notes
* also color code notes according to message severity
2024-06-01 02:47:58 +02:00
Sarjuuk
f77d676a19 Core/Config
* convert configuration from list of constants to object
 * fixes config changes not applying on cli whithout closing and reopening again
 * config variables are no longer embedded in localization text
2024-05-30 20:50:44 +02:00
Sarjuuk
454e09cc78 DB/Errors
* don't need to handle sql warnings in batch when the error handler doesn't use exit() any longer
 * display warnings as [WARN] and use consts instead of magic numbers
2024-05-28 22:40:46 +02:00
Sarjuuk
2d5caba814 DB/Structure
* some more corrections to field types
   - quests: rewardArenaPoints - unsigned -> signed
   - events: unify event id - tinyint -> smallint
   - objects: unify quest id - smallint -> mediumint
   - item_stats: stat cols - smallint -> mediumint (Tester Ring has 64k HP+MP)
2024-05-28 22:37:59 +02:00
Sarjuuk
f422c4ecfb Setup/Spawns
* implement SpawnedByDefault from TC
 * maybe fix a longstanding issue where multiple parent areas per instanced map lead to wrong spawn points
    map  should be                  can also be
    36   The Deadmines              The Great Sea, Unused Ironcladcove
    109  The Temple of Atal'Hakkar  Sunken Temple
    540  The Shattered Halls        Hellfire Citadel
    560  Old Hillsbrad Foothills    Hyjal Past
    631  Icecrown Citadel           The Frost Queen's Lair, Putricide's Laboratory of Alchemical Horrors and Fun, The Crimson Hall, The Frozen Throne, The Sanctum of Blood
2024-05-28 21:58:44 +02:00
Sarjuuk
a87b869896 Setup/BLP
* fix using replacement patch files as images
2024-05-28 20:17:16 +02:00
Sarjuuk
2c451b8deb Locales/Search
* do not apply minimum string length limiter to logographic languages
2024-05-27 17:54:58 +02:00
Sarjuuk
f6565ea924 DB/Structure
* fix data types and data length and add default values where necessary
 * data should no longer get truncated
 * misc fixes
2024-05-22 20:35:02 +02:00
Sarjuuk
7d5930865c DB/Setup
* fix erronous foreign key constraint revealed by MySQL 8.4
2024-05-21 17:46:23 +02:00
Sarjuuk
90b04865f5 Setup/DBC
* use utf8mb4 for dbc_* tables
2024-05-17 23:52:47 +02:00
Sarjuuk
a03d44ae67 ZoneDetailPage/Fishing
* if subzone has no entry in skill_fishing_base_level, try to look up parent entry
2024-05-17 23:27:03 +02:00
Sarjuuk
a97cede1e0 Profiler/Realms
* generally allow all realm types
 * filter visibility/access by userGroup
2024-05-16 22:33:01 +02:00
Sarjuuk
b2690aea08 Setup/Update
* underscores are valid chars for script names
2024-05-15 00:08:45 +02:00
Sarjuuk
c87178c791 PHP/Compat
* spreading a nested array prevents it from being passed as reference
 * this worked previously under php v7.2
2024-05-11 00:32:01 +02:00
Sarjuuk
b5829cc954 Misc/Fixup
* fix reference error from 9a1cb5f2f9
2024-05-10 17:37:13 +02:00
Sarjuuk
9a1cb5f2f9 Setup/ImageGen
* consider preconverted png images when initially checking resources
 * fixed minor bugs
2024-05-09 16:05:23 +02:00
Sarjuuk
d7fa4a900e Admin/Screenshots
* whelp, didn't see the onKeydown handler for the input field
 * fix the fix in bf06c418d4
 * allow "ArrowUp", "ArrowDown" and "-" as inputs
 * also fix search by pressing <Return>
2024-05-07 19:26:06 +02:00
Sarjuuk
bf06c418d4 Misc/Fixup
* do not urlencode mysqli uris  ..  can't wait for this to cause issues again.
 * input[type=number] does not allow inputing dashes to signify negative numbers. Use input[type=text] instead and cast to int afterwards.
2024-05-07 16:43:35 +02:00
Sarjuuk
937bec3d84 Spells/Tooltips
* enable level scaling for spells with RealPointsPerLevel
 * note: While BasePoint vars ($m, $M) can scale, they are often involved in formulas that would have to be recalculated in javascript. This is currently impossible. So this var is skipped for now.
2024-05-06 01:06:31 +02:00
Sarjuuk
99eca2661f Items/Tooltips
* do not display charges if they can't be used
2024-05-05 19:45:27 +02:00
Sarjuuk
6b25288e2b Spells/Reagents
* always check all reagent fields, they are not set first to last
2024-05-05 14:31:15 +02:00
Sarjuuk
92c51237c6 Misc/Fixup
* lost changes from 5f4c62644d
2024-05-04 18:52:56 +02:00
Sarjuuk
5bb277bf2f Tooltips/CombatRatings
* modenize interactive rating string
2024-05-03 21:01:24 +02:00
Sarjuuk
5f4c62644d DB/Errors
* improve db error handling
 * web view should always result in an user friendly error if the db connection is missing or erronous
 * cli use should never fatal if the db connection is erronous. How are you going to fix it, d'uh.
 * if some CLISetup script requires a db connection check it individually
2024-05-03 20:58:30 +02:00
Sarjuuk
41c0af16b3 Misc/Fixup
* drop build steps possibly scheduled to sql update after de2fa377 (they can never be executed)
2024-04-21 22:27:15 +02:00
Sarjuuk
a4c15653d8 DB/SqlModes (#406)
* drop more modes that depend on previously dropped STRICT_TRANS_TABLES
2024-04-21 15:10:35 +02:00
Sarjuuk
42d284dce0 CLI/CR
* always skip \r inputs.
* May fix weird issues when accessing a *nix container from WIN and MacOS hasn't been a thing for decades...
2024-04-20 23:39:29 +02:00
Sarjuuk
d084e6072b DB/SqlModes
* only update sql_mode if strictly necessary
* keep other modes set in my.cfg / my.ini
2024-04-18 19:14:47 +02:00
Sarjuuk
67d4f12cfe Loot/Errors
* gracefully handle loot referencing nonexistent items
2024-04-18 14:12:20 +02:00
Sarjuuk
97d59dbb98 Tooltips/Icons
* properly format icons in tooltips after b125e55a4a
2024-04-03 21:53:52 +02:00
Sarjuuk
f35adfeb3a Misc/Fixups
* fix rogue letter in zhCN localization
 * remove unused redirects from powered tooltips, ocasionally breaking locale detection (preserved for posterity)
2024-04-03 19:31:11 +02:00
Sarjuuk
e5e4446366 Items/Icon
* set a default icon for items so they don't break listviews
2024-04-03 17:49:54 +02:00
Sarjuuk
e01c3ac205 Misc/Fixups
* fix line terminators in update file
2024-04-02 02:03:39 +02:00
Sarjuuk
e09e3a7260 Spells/Effects
* also list affected spells directly in the spell effect
 * allow more spell auras to display affected spells
2024-04-02 02:00:22 +02:00
Sarjuuk
7b43739dbc Localization/NumberFormat
* use narrow non-breaking space (&#8239) to separate thousands blocks for locales frFR and ruRU
2024-03-30 00:37:22 +01:00
SrzmGit
8e5bdebea0 Articles/Class (#397)
* Added german version of the class articles
2024-03-29 20:19:56 +01:00
Sarjuuk
555a6211bd Misc/Fixup
* no idea
2024-03-26 13:33:33 +01:00
Sarjuuk
6249ac6b2a Misc/Fixup
* STDOUT is only defined if php is in run in cli mode
2024-03-26 12:07:45 +01:00
Sarjuuk
494328cf53 DetailPage/Articles
* initialize articles null as originally expected
 * properly sort found articles or the wrong locale may get selected
2024-03-25 14:52:46 +01:00
Sarjuuk
e2e0a0295b ItemDetailPage/Infobox
* properly line break arena requirements after 8bf7b3ee06
 * fix ancient typo in unused code
2024-03-24 22:11:54 +01:00
Sarjuuk
676a7ef24e Misc/Formating
* removed excess ;
2024-03-16 16:38:02 +01:00
Sarjuuk
c01c9ce901 Misc/Fixups
* use built in function to determine if CLI can use escape codes
 * define _post, _get and _cookie in all cases
 * do not apply 8 regexes to a string that doesn't even contain a UI Sequence
2024-03-16 00:17:19 +01:00
Sarjuuk
d37eb9a59b Maps/Repsawn (#172)
* use more verbose time formater for repsawn time
2024-03-16 00:09:56 +01:00
Sarjuuk
d4a0abf704 Types/Objects
* make traps their own category
 * move "trap for" from infobox to listview tabs as there can be a couple hundred GOs pointing to the same trap.
2024-03-15 23:49:58 +01:00
Sarjuuk
ec1a2afc5f PHP/Compat
* fixed misc issues Intellisense was nice enough to highlight.
 * mostly deprecated usage of uninitialized parameters
 * class GenericPage still needs a long, hard look and a refactor
2024-03-11 23:20:17 +01:00
Sarjuuk
3e6e43fd68 MySQL/Compat
* fix col name / function name conflict
2024-03-11 23:20:06 +01:00
Sarjuuk
c37d2fd594 CLI/WIN
* support ansi escape codes for win 10 and up
 * also kill a warning
2024-03-11 23:16:32 +01:00
Sarjuuk
25b5928a22 ItemFilter/TabLinks
* always display FilterResult prompt on currency-for tabs
 * fixed display of FilterResult prompt on item currency-for tab
2024-03-09 22:44:55 +01:00
Sarjuuk
88b62730e1 SmartAI/Misc
* fix footer for ACTION_FOLLOW
 * use \u003A for : so it doesn't get evaluated as a switch
2024-03-06 21:26:39 +01:00
Sarjuuk
f00ccedbd3 Localization/Invisibility
* invisibility type General is on index 0 not index 1.
2024-03-06 21:25:44 +01:00
Sarjuuk
de2fa3770b JS/Tooltips
* try to get current domain from Locale when used on the site itself and no other params are given
2024-02-29 18:12:46 +01:00
Sarjuuk
be06b1e0cf Items/ExtraLoot
* corrected calculation after writing a simulator and some consultation
 * sad thing is, this will not even be visible after rounding for display
2024-02-29 17:33:55 +01:00
Sarjuuk
29f80f9a76 Items/ExtraLoot
* break down chances for loot from skill_extra_item_template
 * rename tab to make it clear that its content is a bonus
2024-02-29 01:25:34 +01:00
Sarjuuk
e85a9e9d6a Setup/Zones
* instances with with a entrance touple of (0, 0, 0) will no longer be displayed somewhere in Alterac (at the map 0 origin point)
2024-02-28 22:17:07 +01:00
Sarjuuk
54b224d929 Achievements/Rewards
* fix createing bogus ItemLists or TitleLists when displaying achievement rewards
 * don't display empty related-achievements tab on Emotes DetailPage
2024-02-28 21:52:34 +01:00
Sarjuuk
d0e5bec845 Setup/FileGen (#390)
* directories should probably have write permissions
 * add forgotten directory for guide image storage
2024-02-28 21:18:05 +01:00
Sarjuuk
b125e55a4a Custom/UiEscapes
* so, apparently the client allows ui escape sequences in the weirdest of places. Guess i have to follow suit.
 * also, the sequences can be multiline
 * also also, the file extension for icons is optional
2024-02-28 21:04:18 +01:00
Sarjuuk
611d2c40bd Profiler/Access
* load css/js even when profiler is disabled
 * do not display page speciffic announcements for overridden displays (e.g. Profiler help on maintenance, error, etc. display)
2024-02-28 19:05:21 +01:00
Sarjuuk
8b1fd3ac79 Setup/Source
* setting an unsigned column -1 is pretty pointless, isn't it?
 * using a recently truncated, not yet repopulated table to determine if items can be obtained is pretty pointless, RIGHT!?
   (items are not longer blanket tagged as unobtainable)
2024-02-28 19:05:17 +01:00
Sarjuuk
9831038729 Misc/Fixups
* remove w from umask for generated files
 * fixed warning in setup dbconfig
 * added lost Markup.js generation to initial setup
2024-02-28 19:05:17 +01:00
Sarjuuk
8bbffae837 Setup/DB
* fixed syntax error in initial db setup
 * corrected class role custom data
2024-02-28 16:28:06 +01:00
Amandil
b2ca072120 Misc/Fixups (#391)
* lost spell effect/aura declaration changes from 0e0116b27
     * case-sensitivity conflict (ID => id) caused by 77f81c1bd
2024-02-28 15:57:55 +01:00
Sarjuuk
9aeb2177cf Pages/SpellDetailPage
* fixed "modefied-by" and "modifies" tab having different results
 * evalueate some more spell effects
 * replace magic numbers with defined strings
 * get model info from some more spell auras
2024-02-26 01:23:35 +01:00
Sarjuuk
3dfdc300c5 DetailPage/Links (#388)
* add missing trailing slash to url in links list
2024-02-25 22:41:16 +01:00
Sarjuuk
4e65f0a955 Merge remote-tracking branch 'github/master' into ghMaster 2024-02-25 22:31:33 +01:00
Sarjuuk
dd9eaf49ff Page/Listview
* refer to Listview template by default name if able
2024-02-25 21:34:03 +01:00
Sarjuuk
6b0f617d1b Page/Screenshots
* handle more error cases more gracefully when uploading screenshots
2024-02-25 21:28:36 +01:00
Sarjuuk
ba53a5c760 Page/WH
* handle backlink in Page instead of Util
 * update event/holiday link
2024-02-25 21:26:30 +01:00
Sarjuuk
6958efcd0f Items/Tooltip
* do not show cooldown for passive onEquip spells
2024-02-25 21:24:36 +01:00
Sarjuuk
85e8175338 Spells/Misc
* try to handle bogus data from creature_template_addon.auras
2024-02-25 21:23:33 +01:00
Sarjuuk
a14b5e2be1 Libs/DBSimple
* also collect sql warnings for error handling
 * urlescape user/password on the db connection url (mysqli://)
2024-02-25 21:23:09 +01:00
Sarjuuk
d8a6f67688 Misc/Timer
* calling reset() on the Timer no longer breaks the interval
2024-02-25 21:19:10 +01:00
Sarjuuk
979a21afae Localization/Caching
* store source lists as object so different locales can be fetched
 * store parsed spell texts locale dependent so the locale isn't fixed
2024-02-25 21:19:04 +01:00
Sarjuuk
8269d4946f Misc/Home
* update currentyear
2024-02-25 21:18:52 +01:00
Sarjuuk
a39881e73f Misc/Fixup
* fixed borked Lang::load in setup after d0d2451ff51157728e622142c3be7ae0ba7dcebe
2024-02-25 21:17:33 +01:00
Sarjuuk
cfa5030f85 Profiler/ArenaTeams
* remove profiles from existing teams of the same type they are going to be added to.
 * should prevent characters being stuck in old teams.
2024-02-25 21:15:33 +01:00
Sarjuuk
c84d1181bb Localization
* add translation for dungeon floors from GlobalStrings.lua
 * fix SpellMod mistranslation
2024-01-08 00:12:30 +01:00
Sarjuuk
d92b17a386 Loot/Difficulty
* fixed encoding Dungeon Difficulty in source
 * resolve difficulty dummy loot sources to base creature if able
 * added listview note for difficulty source on item detail page
 * misc: fixed parsing color UI escape sequence
2024-01-08 00:06:30 +01:00
Sarjuuk
9e857035bd Creatures/Filter
* implement filter #34 (modelId)
 * thats the actual modelId .. not displayId
2024-01-08 00:05:32 +01:00
Sarjuuk
79383fc83a Search/Opensearch
* return results with correct mime type.
2024-01-08 00:05:32 +01:00
Sarjuuk
0e0116b274 Misc/Defines
* declare and use Spell Effects and Spell Auras
2024-01-08 00:05:32 +01:00
Sarjuuk
deba5325a7 Spells/DetailPage
* display proc info for dummy auras
2024-01-08 00:05:32 +01:00
Sarjuuk
a73d71b966 Profiler/Pets
* fix talent distribution being converted to int
* store pets tamed in heroic dungeons/raids with its base entry
2024-01-08 00:05:32 +01:00
Sarjuuk
06ecfd93d5 Setup/DBC
* add Item.dbc (maybe use later for creature eqipment display)
2024-01-08 00:05:32 +01:00
Sarjuuk
ac34b47c26 Spells/Sounds
* get sounds from ScreenEffect.dbc and link to type Spell and type Sound
 * resolve ScreenEffect name in SpellDetailPage
Fixup
 * fix warning in UI escape sequence parsing
2024-01-08 00:05:32 +01:00
Sarjuuk
9b16f2d84a Localization/WoW strings
* generalize WoW UI escape sequence handling
 * implement use of declinated words from dbc for locale 8 (ruRU)
2024-01-08 00:05:24 +01:00
Sarjuuk
cc594e3415 Skills/Requirements
* fix requirements  display for low level skinning
 * display fishing skill requirements on zone detail page in fishing tab
2024-01-07 23:04:03 +01:00
Sarjuuk
4d6fb4975e Emotes
* have creature emotes in the same system (on a negeative index) as SAI links were uselessly pointing to player emotes before
 * player emotes are now gendered
 * display two more cases of who points at whom when using a player emote
 * use and link event sound from emote
 * display more misc info
2024-01-07 23:03:21 +01:00
Sarjuuk
0f186576d3 Filter/Fixups
* fixed wrong indizes for gem color picker in locale 3 (deDE)
 * fixed interpretation of filter enums. Some are providing their value directly, others have an intermediate key.
 * createSQLForCriterium is no longer abstract. (was implemented in exactly the same way everywhere)
 * erronous criteria are now logged and discarded entirely)
2024-01-07 22:53:38 +01:00
Sarjuuk
837fdf78a0 Zone/DetailPage
* fix quests tab not displaying quests from zone
 * only include special quests (QuestSortID set) and quests directly related to zone instead of any quest in tab
 * adjust quest rewards tab accordingly
 * fix breadcrumbs for dungeon quests with sub category
2023-06-08 21:09:48 +02:00
Sarjuuk
6382302a30 Defines/SpellTrigger
* convert magic numbers to define and use as spell trigger types
2023-06-08 15:35:51 +02:00
Sarjuuk
da8943095b Localization/Typo
* introduced about 10 years ago *nostagia*
2023-06-08 15:35:42 +02:00
Sarjuuk
26da03f029 Libs/jQuery
* upgrade from v1.12.4 to v3.7.0
2023-06-08 15:35:31 +02:00
Sarjuuk
73dd745fe8 Profiler/Achievementpoints
* remove redundant total calculation as it is already stored with the profile
 * do not include statistic in sum calculation. (ffs why do they even have points assigned)
2023-06-08 15:35:19 +02:00
Sarjuuk
eca3e09482 Types/Filter
* do not split strings at \s if match:exact is set.
2023-06-08 15:35:07 +02:00
Sarjuuk
d8d2676596 Profiler/Completion
* move spells w/o source to excludeGroup unavailable
   shouldn't hide. They may have been available in the past.
2023-06-08 15:34:59 +02:00
Sarjuuk
a6f6e0b05d Itemset/Listpage
* implement filter: available to players
2023-06-08 15:30:44 +02:00
Sarjuuk
e9622e6991 Filter/Cleanup
* move shared criteria enums to parent
 * define shared regex patterns filter
 * set missing enum checks
 * fixed some filters
2023-06-08 15:28:57 +02:00
Sarjuuk
fcf24b3a45 Misc
* define and use some more magic numbers
 * move id-based custom data from spell SetupScript to aowow_setup_custom_data table
 * hide on unused glyph while at it
2023-06-08 15:27:49 +02:00
Sarjuuk
77f81c1bde Setup/Sources
* rewrote SetupSrcipt
 * implemented 'zone' parameter
 * implemented 'bossdrop' parameter
 * implemented 'dungeondifficulty' parameter
 * implemented item filter relying on zone information (dropsInX)
 * fixed world random drops showing a single loot source
 * extended Source column of spells to the same functionality as items

ToDo:
 * apply new 'commondrop' parameter on loot listviews
 * gather difficuly versions of gameobjects and apply the same logic as for creatures
 * implement fake spawns so npcs can get linked to a zone
2023-06-08 15:07:13 +02:00
Sarjuuk
fc7a526a67 Items/ListPage
* fixed displaying icon for currencies in vendor related columns
2023-06-08 14:16:29 +02:00
Sarjuuk
e71da620c6 Comments/Replies
* also show replies on latest-comments utility page
 * sort replies by score DESC
2023-06-08 14:16:03 +02:00
Sarjuuk
0d6a6e163c Profiler/Statistics
* use spells for skill modifying racials instead of hardcoded values
 * get baseline statistics from player_levelstats instead of hardcoded values
2023-06-08 14:01:36 +02:00
Sarjuuk
1c5e43d378 Profiler/Completion
* apply factions from player_factionchange_spells to all spells
 * filter companion pets without suitable item source
2023-06-08 14:00:32 +02:00
Sarjuuk
d16d685b70 Guides/Markup
* promote user guides to blogger level so the markup promised by the editor is actually appied.
2023-06-08 13:58:53 +02:00
Sarjuuk
856a98d875 Profiler/Characters
* fixed position of pending rename note
2023-06-08 13:55:35 +02:00
Sarjuuk
bc3ba23162 Guides/Fixups
* fix urls in user menu
 * strip anchors from tooltip title
 * prevent line breaks in description
 * make only in english info popup modular
2023-06-08 13:53:48 +02:00
Sarjuuk
0e9ca3ff90 Quests/Filter
* filter "repeatable" now also considers specialFlags
2023-06-08 13:48:56 +02:00
Sarjuuk
24d683332d Profiler/Completion
* fix partial loading of quests in profiler
 * provide inital category totals on a per class & race basis
2023-06-08 13:45:55 +02:00
Sarjuuk
ebc20be508 Spells/Localization
* use more GlobalStrings in spell context (as far as possible)
 * fixed rune cost order
 * fixes agains locale based time formater
2023-06-08 13:03:05 +02:00
Sarjuuk
8bf7b3ee06 Lang/cleanup
* fixed break and trim functions not handling text shorter than break length
 * add option to output raw text besides html, markup format
 * decalare return types and parameter types
 * cleanup
2023-06-08 12:56:36 +02:00
Sarjuuk
78f7f6b9cf Page/Profile
* fixed hang on profile load without base data
2023-06-08 12:55:06 +02:00
Sarjuuk
bfb7abb843 Profiler/Progression
* reorient icon texture so instance icons no longer appear multiple times on long progress bars
2023-06-08 12:52:54 +02:00
Sarjuuk
70a93d9905 Misc/Lang
* remove some more square brackets denoting missing translations
2023-06-07 19:46:52 +02:00
Stefano Borzì
153d489400 Lang/zhCN (#384)
* cherry pick missing translation from https://github.com/azerothcore/aowow/pull/36
---------

Co-authored-by: yuanf225 <43561197+yuanf225@users.noreply.github.com>
2023-06-06 21:49:23 +02:00
Sarjuuk
40014755e2 Setup/Log
* allow overwriting generic/fine log output
 * fix some errors
 * can't catch notices generated by mysqli_connect
 * removed some unnessecary ORDER from querys
2023-04-25 23:26:25 +02:00
Sarjuuk
acb9c60c9a Profiler/QuickInfo
* remove padding from profiles icon list
2023-04-25 23:23:49 +02:00
Sarjuuk
934066fed8 Tooltips/Style
* also fix width for secondary tooltips
2023-04-25 23:23:41 +02:00
Sarjuuk
e916deaafc Setup/Fixups
* sanity check slot / invtype realtion on setup (and hide offenders)
 * hide internal/unused items by name part
 * always truncate table to get rid of old data
 * fixing one->many relation revealed by replacing REPLACE with INSERT in creature setup
2023-04-25 23:23:19 +02:00
Sarjuuk
1130581152 Items/Stats
* resolve more col name conflicts .. should probably rename the cols in the table itself...
2023-04-20 22:04:25 +02:00
Sarjuuk
5c227c01e4 Currency/PvP
* convert and assign correct icons for honor points and arena points from /Interface/PvPFrame
2023-04-20 19:53:22 +02:00
Sarjuuk
a6897ad2f8 Misc/Fixups
* fixed more error pages without styles/scripts
 * fixed sourcemore for items if more then 300 items where requested at once
 * fixed item name localization in rare cases
2023-04-20 19:51:46 +02:00
Sarjuuk
5be5c2b59e Guides/Listview
* display class/spec in category if applicable
 * make class/spec searchable
 * unify class/spec display with tooltips
2023-04-18 17:20:40 +02:00
Sarjuuk
9c8656f4b5 Admin/Guides
* log approver / time as intended
2023-04-18 15:45:01 +02:00
Sarjuuk
14658a5016 Pages/Scripts
* do not skip generic page constructor ... ever
2023-04-15 11:46:21 +02:00
wodim
5f708470fc Localization/Typo (#354)
* small typo
2023-04-13 22:04:24 +02:00
Sarjuuk
f2a0e75bb1 Scripts/Fixup
*  fix tooltip localization after 9f1cbc0233
2023-04-13 19:26:23 +02:00
Sarjuuk
4f13c492f3 Pages/Icon (#360)
* fixed missing icons in used-by-spell tab
2023-04-13 17:48:10 +02:00
Sarjuuk
6123b6bafc JS/jQuery
* workaround jQuery.htmlPrefilter vulnerability
2023-04-13 17:34:52 +02:00
Sarjuuk
2c142c506c Search/Forms (#383)
* do not prefil search form with unchecked user input
 * thx @Endalaust
2023-04-13 17:34:39 +02:00
Sarjuuk
9f1cbc0233 HTML/Scripts
* append a filemtime timestamp to js/css files to work around browser caching after update
 * shuffle script handling around a bit
 * also user pages cant have community content
 * also fix breadcrumbs on items page
2023-04-13 17:30:23 +02:00
Sarjuuk
b06d1a5c2c Pages/Caching (#380)
* move localized option sorting to postCache() to prevent real order display in cached versions
2023-04-13 17:26:27 +02:00
Sarjuuk
beb32da3b4 Itemset/Summary (#351)
* disable compare button for empty item sets
 * do not display summary for empty item sets
2023-04-13 17:25:42 +02:00
Sarjuuk
2e82bf84d2 Pages/Spell (#378)
* fixed effect index offset on DetailPage
2023-04-13 17:25:31 +02:00
Sarjuuk
25ddb0ca99 Items/FAP
* show Feral Attack Power on weapons a druid can actually use (incuding 1H-weapons, excluding swords & axes (usually))
2023-04-13 17:25:17 +02:00
Sarjuuk
02239b4f74 Misc/Fixups
* fix text vars in tooltips for missing entities
 * fix notice in weapon GS calculation
 * reduce tooltip max width slightly to prevent overlap with secondary tooltip
2023-04-13 17:21:55 +02:00
Sarjuuk
e0a3c44776 Setup/Spells (#374)
* fixed rarity color for spells that create items
2023-04-13 17:20:50 +02:00
Sarjuuk
e513e01b29 Setup/TalentIcons (#382)
* fix icon order for pet talents
2023-04-13 17:16:49 +02:00
Sarjuuk
138dbbc8a5 Game/Text
* try to catch more html-like constructs in text blobs and escape them
 * ...<hic>!
2023-04-13 17:16:01 +02:00
Sarjuuk
6f87870e09 Item/Tooltips (#368)
* fixed broken link to required event
2023-04-13 17:15:53 +02:00
Sarjuuk
2210c0c4c5 Spells/Auras (#379)
* get auras from creature_template_addon for tabs
   - npc: abilities
   - spell: used by npc
2023-04-13 17:15:28 +02:00
Sarjuuk
8ab8eee1f4 Tooltips
* revert e092a69175 as it causes the tooltip to grow when moving the mouse.
 * use CSS to solve
2023-04-13 17:13:38 +02:00
Sarjuuk
d77e459da3 Tooltips/TimeFMT
* use strings from globalstrings.lua to format time in item and spell tooltips
 * fixed item filter ItemCooldown
 * fixed timeAgo format
 * move item duration to correct position in tooltip
2023-04-13 17:12:58 +02:00
Sarjuuk
6ee0d63766 Update external links to point to wotlk speciffic database 2023-04-13 17:12:02 +02:00
Sarjuuk
0c47f262ea Userdata
* replace input filter FILTER_UNSAFE_RAW (+ STRIP flags) with regex checks to preserve \n and utf8 chars
2023-04-13 17:07:59 +02:00
Sarjuuk
ffa4cf5b29 Misc/Fixes
* resolve more spellEffects/Auras
 * fixed item filter with multiple upgrade items
 * localized unknown spellAura/Effects text
 * remove unreferenced error code from image setup
2023-04-13 17:05:30 +02:00
Sarjuuk
ca26955ac2 Setup/SQL
* change sql batching to account for non continuous indizes in world tables
2023-04-13 16:48:52 +02:00
Sarjuuk
f05fe60030 Spells/Tooltips
* fix level slider for spells with buffs but without tooltips
2023-04-13 16:31:09 +02:00
Sarjuuk
2b15c13e88 User/Guides
* fixed copy/paste fail
2023-04-13 16:16:58 +02:00
Sarjuuk
df1ba841c5 Item/Tooltip
* display hidden/cosmetic spells for staff
2022-06-10 16:58:18 +02:00
Sarjuuk
47da18b717 Profiler/Caching
* redirect to search for subjects without cache, instead of hanging on an empty profile
 * todo: fix properly
2022-06-10 16:41:03 +02:00
Sarjuuk
117ab617b6 SAI/Teleport (#346)
* fixed resolving teleport target for NPCs
2022-06-10 16:00:33 +02:00
Sarjuuk
7e5659f49f NPC/Waypoints
* improve display for NPCs traversing zones and/or floors (e.g.: npc 844)
2022-06-10 15:15:28 +02:00
Sarjuuk
e493acca0d Item/XML
* fix output for currency, quest and key subclasses
2022-06-10 14:40:30 +02:00
Sarjuuk
6de6853cfe Core/PHP
* bump version requirements to v8.0
2022-06-10 14:00:35 +02:00
Sarjuuk
8ec6cc548d Merge remote-tracking branch 'origin/master' 2022-06-10 13:58:55 +02:00
Sarjuuk
c0e9159c1e Misc/Fixups
* colAddins should be null if not in use
 * fix index warnings in search + profiler
 * cast url param 'locale' to int for all uses
 * fix breadcrumbs for sounds page
 * fix determining actionOwner for SmartAI
2022-06-10 13:58:13 +02:00
Dima
1bd752a60f Typo/Spells (#343)
* fixed legacy typo in spell power calculation
2022-05-30 16:27:56 +02:00
Sarjuuk
df3694b539 Setup/Mails (#340)
* fix borked setup after b3e8f5e50f
 * thx @jackpoz for the research
2022-05-08 00:22:10 +02:00
Sarjuuk
6594d6fa42 Spells/Tooltips
* htmlify \n in spell tooltips & buffs
 * allow recursion of Util::parseHtmlText()
2022-04-05 18:32:41 +02:00
Sarjuuk
8425eeb685 Reports
* move related functions to separate class
 * implement out of date comment handling shorthand
 * implement admin interface to work with reports
   - listing based on user group
   - assign to self / unassign from self
   - close with reason
   - comment functionality
   - reward reputation to creator based on resolution
2022-04-05 15:45:56 +02:00
HelloKitty
2daa724720 README/Typo (#337)
Fix typo in the Special Thanks section.
2022-03-30 13:18:16 +02:00
Sarjuuk
6db77ed4f2 CommunityContent/Listing
* generally use indexed/non-asssociative arrays when returning content
2022-03-28 16:25:09 +02:00
Sarjuuk
b08d30d043 Screenshots/Crop
* fixed coordinate filter broken after a8edf6c912
2022-03-28 00:58:43 +02:00
Sarjuuk
b3e8f5e50f DB/Mails
* add field cuFlags to Type:Mail storage (RandomPage search no longer randomly fails, when randomly selecting mails)
2022-03-25 16:41:58 +01:00
Sarjuuk
1f5e2645f0 Misc/Fixups
* forgotten rename fails
2022-03-25 16:41:58 +01:00
Sarjuuk
04e55b5498 NPCs/Vendors
* show restock time if available
2022-03-25 16:41:49 +01:00
Sarjuuk
32b4c451e4 Misc/Fixup
* remove unnecessary tab components from ?unrated-comments page
 * fix urls generated by guides listview
2022-03-24 10:38:45 +01:00
Sarjuuk
05f6d68070 Utility
* implemented page: unrated-comment
2022-03-23 19:28:40 +01:00
Sarjuuk
e572967c08 Misc/Fixup
* lost changes to timestamps from a8edf6c912
* dates on comment, screenshot, video previews are now formatted correctly
2022-03-23 11:19:00 +01:00
Sarjuuk
1dc8d50289 Misc/Fixup
* move guide localization, so Lang::typeName can pick it up
 * rename forgotten TYPE_* definitions
2022-03-22 22:27:19 +01:00
Sarjuuk
7caabc0fa8 Misc/Fixup
* obligatory after-the-fact fixups
2022-03-22 19:07:47 +01:00
Sarjuuk
d819bf60f5 Merge remote-tracking branch 'origin/master' 2022-03-22 15:57:18 +01:00
Sarjuuk
65bfd93761 Profiler
* truncate local profile if sync with game server fails (entry deleted)
 * do not use chars/guilds/arena teams with empty names
2022-03-13 15:03:11 +01:00
Sarjuuk
3a98201837 Zones/Locations
* fixed Quick Info defaulting expansion maps to have [0, 0, 0] as parent
 * corrected looking for custom excluded flag when collecting instances for area
2022-03-08 18:52:01 +01:00
1012 changed files with 112939 additions and 84295 deletions

10
.gitattributes vendored
View File

@@ -1,4 +1,14 @@
* text=input
*.php text eol=lf
*.js text eol=lf
*.css text eol=lf
*.sql text eol=lf
aowow text eol=lf
prQueue text eol=lf
*.png binary
*.jpg binary
*.gif binary
*.ttf binary
*.swf binary

9
.gitignore vendored
View File

@@ -3,16 +3,15 @@
# cache
/cache/template/*
/setup/generated/alphaMaps/*.png
/cache/firstrun
/cache/alphaMaps/*
/cache/setup/*
# extract from MPQ
/setup/mpqdata/*
# generated files
/static/js/profile_all.js
/static/js/locale.js
/static/js/Markup.js
/static/js/global.js
/static/widgets/power.js
/static/widgets/power/demo.html
/static/widgets/searchbox.js
@@ -50,4 +49,4 @@
/static/uploads/screenshots/*
/static/uploads/signatures/*
/static/uploads/temp/*
/static/uploads/guide/images/*

View File

@@ -13,20 +13,21 @@ While the first releases can be found as early as 2008, today it is impossible t
This is a complete rewrite of the serverside php code and update to the clientside javascripts from 2008 to something 2013ish.
I myself take no credit for the clientside scripting, design and layout that these php-scripts cater to.
Also, this project is not meant to be used for commercial puposes of any kind!
Also, this project is not meant to be used for commercial purposes of any kind!
## Requirements
+ Webserver running PHP ≥ 7.4 — 8.0 including extensions:
+ Webserver running PHP ≥ 8.2 including extensions:
+ [SimpleXML](https://www.php.net/manual/en/book.simplexml.php)
+ [GD](https://www.php.net/manual/en/book.image)
+ [MySQL Improved](https://www.php.net/manual/en/book.mysqli.php)
+ [Multibyte String](https://www.php.net/manual/en/book.mbstring.php)
+ [File Information](https://www.php.net/manual/en/book.fileinfo.php)
+ [Internationalization](https://www.php.net/manual/en/book.intl.php)
+ [GNU Multiple Precision](https://www.php.net/manual/en/book.gmp.php) (When using TrinityCore as auth source)
+ MySQL ≥ 5.5.30
+ [TDB 335.21101](https://github.com/TrinityCore/TrinityCore/releases/tag/TDB335.21101)
+ MySQL ≥ 5.7.0 OR MariaDB ≥ 10.6.4 OR similar
+ [TDB 335.21101](https://github.com/TrinityCore/TrinityCore/releases/tag/TDB335.21101) (no other other providers are supported at this time)
+ WIN: php.exe needs to be added to the `PATH` system variable, if it isn't already.
+ Tools require cmake: Please refer to the individual repositories for detailed information
+ [MPQExtractor](https://github.com/Sarjuuk/MPQExtractor) / [FFmpeg](https://ffmpeg.org/download.html) / (optional: [BLPConverter](https://github.com/Sarjuuk/BLPConverter))
@@ -39,7 +40,7 @@ audio processing may require [lame](https://sourceforge.net/projects/lame/files/
#### Highly Recommended
+ setting the following configuration values on your TrinityCore server will greatly increase the accuracy of spawn points
> Calculate.Creature.Zone.Area.Data = 1
> Calculate.Gameoject.Zone.Area.Data = 1
> Calculate.Gameobject.Zone.Area.Data = 1
## Install
@@ -50,7 +51,7 @@ audio processing may require [lame](https://sourceforge.net/projects/lame/files/
#### 2. Prepare the database
Ensure that the account you are going to use has **full** access on the database AoWoW is going to occupy and ideally only **read** access on the world database you are going to reference.
Import `setup/db_structure.sql` into the AoWoW database `mysql -p {your-db-here} < setup/db_structure.sql`
Import files 01 - 03 from `setup/sql/` in order into the AoWoW database `mysql -p {your-db-here} < setup/sql/01-db_structure.sql`, etc.
#### 3. Server created files
See to it, that the web server is able to write the following directories and their children. If they are missing, the setup will create them with appropriate permissions
@@ -72,19 +73,19 @@ Extract the following directories from the client archives into `setup/mpqdata/`
.. once is enough (still apply the localeCode though):
> \<localeCode>/Interface/TalentFrame/
> \<localeCode>/Interface/Glues/Credits/
> \<localeCode>/Interface/Icons/
> \<localeCode>/Interface/Spellbook/
> \<localeCode>/Interface/PaperDoll/
> \<localeCode>/Interface/GLUES/CHARACTERCREATE/
> \<localeCode>/Interface/Glues/CharacterCreate/
> \<localeCode>/Interface/Pictures
> \<localeCode>/Interface/PvPRankBadges
> \<localeCode>/Interface/FlavorImages
> \<localeCode>/Interface/Calendar/Holidays/
> \<localeCode>/Sound/
.. optionaly (not used in AoWoW):
> \<localeCode>/Interface/GLUES/LOADINGSCREENS/
.. optionally (not used in AoWoW):
> \<localeCode>/Interface/Glues/Loadingscreens/
> \<localeCode>/Interface/Glues/Credits/
#### 5. Reencode the audio files
WAV-files need to be reencoded as `ogg/vorbis` and some MP3s may identify themselves as `application/octet-stream` instead of `audio/mpeg`.
@@ -100,24 +101,24 @@ When you've created your admin account you are done.
## Troubleshooting
Q: The Page appears white, without any styles.
A: The static content is not being displayed. You are either using SSL and AoWoW is unable to detect it or STATIC_HOST is not defined poperly. Either way this can be fixed via config `php aowow --siteconfig`
A: The static content is not being displayed. You are either using SSL and AoWoW is unable to detect it or STATIC_HOST is not defined properly. Either way this can be fixed via config `php aowow --siteconfig`
Q: Fatal error: Can't inherit abstract function \<functionName> (previously declared abstract in \<className>) in \<path>
A: You are using cache optimization modules for php, that are in confict with each other. (Zend OPcache, XCache, ..) Disable all but one.
A: You are using cache optimization modules for php, that are in conflict with each other. (Zend OPcache, XCache, ..) Disable all but one.
Q: Some generated images appear distorted or have alpha-channel issues.
A: Image compression is beyond my understanding, so i am unable to fix these issues within the blpReader.
BUT you can convert the affected blp file into a png file in the same directory, using the provided BLPConverter.
AoWoW will priorize png files over blp files.
AoWoW will prioritize png files over blp files.
Q: How can i get the modelviewer to work?
A: You can't anymore. Wowhead switched from Flash to WebGL (as they should) and moved or deleted the old files in the process.
Q: I'm getting random javascript errors!
A: Some server configurations or external services (like Cloudflare) come with modules, that automaticly minify js and css files. Sometimes they break in the process. Disable the module in this case.
A: Some server configurations or external services (like Cloudflare) come with modules, that automatically minify js and css files. Sometimes they break in the process. Disable the module in this case.
Q: Some search results within the profiler act rather strange. How does it work?
A: Whenever you try to view a new character, AoWoW needs to fetch it first. Since the data is structured for the needs of TrinityCore and not for easy viewing, AoWoW needs to save and restructure it locally. To this end, every char request is placed in a queue. While the queue is not empty, a single instance of `prQueue` is run in the background as not to overwhelm the characters database with requests. This also means, some more exotic search queries can't be run agains the characters database and have to use the incomplete/outdated cached profiles of AoWoW.
A: Whenever you try to view a new character, AoWoW needs to fetch it first. Since the data is structured for the needs of TrinityCore and not for easy viewing, AoWoW needs to save and restructure it locally. To this end, every char request is placed in a queue. While the queue is not empty, a single instance of `prQueue` is run in the background as not to overwhelm the characters database with requests. This also means, some more exotic search queries can't be run against the characters database and have to use the incomplete/outdated cached profiles of AoWoW.
Q: Screenshot upload fails, because the file size is too large and/or the subdirectories are visible from the web!
A: That's a web server configuration issue. If you are using Apache you may need to [enable the use of .htaccess](http://httpd.apache.org/docs/2.4/de/mod/core.html#allowoverride). Other servers require individual configuration.
@@ -134,7 +135,7 @@ A: A search is only conducted against the currently used locale. You may have on
## Special Thanks
Said website with the red smiling rocket, for providing this beautifull website!
Please do not reagard this project as blatant rip-off, rather as "We do really liked your presentation, but since time and content progresses, you are sadly no longer supplying the data we need".
Said website with the red smiling rocket, for providing this beautiful website!
Please do not regard this project as blatant rip-off, rather as "We do really liked your presentation, but since time and content progresses, you are sadly no longer supplying the data we need".
![uses badges](http://forthebadge.com/images/badges/uses-badges.svg)

17
aowow
View File

@@ -1,12 +1,15 @@
#!/usr/bin/env php
<?php
require 'includes/shared.php';
if (!CLI)
if (PHP_SAPI !== 'cli')
die("this script must be run from CLI\n");
if (CLI && getcwd().DIRECTORY_SEPARATOR.'aowow' != __FILE__)
die("this script must be run from root directory\n");
else
require 'setup/setup.php';
if (PHP_SAPI === 'cli' && getcwd().DIRECTORY_SEPARATOR.'aowow' != __FILE__)
die("this script must be run from the aowow root directory\n");
require_once 'includes/kernel.php';
require_once 'includes/setup/cli.class.php';
require_once 'includes/setup/timer.class.php';
require_once 'setup/setup.php';
?>

View File

@@ -4,13 +4,16 @@ if (!defined('AOWOW_REVISION'))
die('illegal access');
function extAuth($user, $pass, &$userId = 0, &$userGroup = -1)
function extAuth(string &$usernameOrEmail, #[\SensitiveParameter] string $password, int &$userId = 0, int &$userGroup = -1) : int
{
/*
insert some auth mechanism here
see defines for usable return values
set userId for identification
set usernameOrEmail to a valid username, do not pass back the email if used for login
set userId to uid from external auth provider for identification
(optional) set userGroup to a valid userGroup (see U_GROUP_* defines)
return an AUTH_* result (see defines)
*/
return AUTH_INTERNAL_ERR;

View File

@@ -1,46 +0,0 @@
Mapper.multiLevelZones = {
206: ['206-1', '206-2', '206-3'],
209: ['209-1', '209-2', '209-3', '209-4', '209-5', '209-6', '209-7'],
616: ['616-1', '616_1', '616_2'],
719: ['719-1', '719-2', '719-3'],
721: ['721-1', '721-2', '721-3', '721-4'],
796: ['796-1', '796-2', '796-3', '796-4'],
1196: ['1196-1', '1196-2'],
1337: ['1337-1', '1337-2'],
1581: ['1581-1', '1581-2'],
1583: ['1583-1', '1583-2', '1583-3', '1583-4', '1583-5', '1583-6', '1583-7'],
1584: ['1584-1', '1584-2'],
2017: ['2017-1', '2017-2'],
2057: ['2057-1', '2057-2', '2057-3', '2057-4'],
2100: ['2100-1', '2100-2'],
2557: ['2557-1', '2557-2', '2557-3', '2557-4', '2557-5', '2557-6'],
2677: ['2677-1', '2677-2', '2677-3', '2677-4'],
3959: ['3959', '3959-1', '3959-2', '3959-3', '3959-4', '3959-5', '3959-6', '3959-7'],
3428: ['3428-1', '3428-2', '3428-3'],
3456: ['3456-1', '3456-2', '3456-3', '3456-4', '3456-5', '3456-6'],
3457: ['3457-1', '3457-2', '3457-3', '3457-4', '3457-5', '3457-6', '3457-7', '3457-8', '3457-9', '3457-10', '3457-11', '3457-12', '3457-13', '3457-14', '3457-15', '3457-16', '3457-17'],
3477: ['3477-1', '3477-2', '3477-3'],
3715: ['3715-1', '3715-2'],
3790: ['3790-1', '3790-2'],
3791: ['3791-1', '3791-2'],
3848: ['3848-1', '3848-2', '3848-3'],
3849: ['3849-1', '3849-2'],
4075: ['4075', '4075-1'],
4100: ['4100-1', '4100-2'],
4131: ['4131-1', '4131-2'],
4196: ['4196-1', '4196-2'],
4228: ['4228-1', '4228-2', '4228-3', '4228-4'],
4272: ['4272-1', '4272-2'],
4273: ['4273-0', '4273-1', '4273-2', '4273-3', '4273-4', '4273-5'],
4277: ['4277-1', '4277-2', '4277-3'],
4395: ['4395-1', '4395-2'],
4494: ['4494-1', '4494-2'],
4714: ['4714-1', '4714_1', '4714_2', '4714_3'],
4722: ['4722-1', '4722-2'],
4812: ['4812-1', '4812-2', '4812-3', '4812-4', '4812-5', '4812-6', '4812-7', '4812-8'],
};
/*
var g_zone_areas = {};
in locale files
*/

View File

@@ -0,0 +1,34 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AboutusBaseResponse extends TemplateResponse
{
protected string $template = 'text-page-generic';
protected string $pageName = 'aboutus';
protected ?int $activeTab = parent::TAB_MORE;
protected array $breadcrumb = [2, 0];
public function __construct(string $pageParam)
{
parent::__construct($pageParam);
if ($pageParam)
$this->generateError();
}
protected function generate() : void
{
$this->h1 = Lang::main('moreTitles', $this->pageName);
array_unshift($this->title, $this->h1);
parent::generate();
}
}
?>

View File

@@ -0,0 +1,177 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AccountBaseResponse extends TemplateResponse
{
protected string $template = 'account';
protected string $pageName = 'account';
protected array $scripts = [[SC_JS_FILE, 'js/account.js']];
// display status of executed step (forwarding back to this page)
public ?array $generalMessage = null;
public ?array $emailMessage = null;
public ?array $usernameMessage = null;
public ?array $passwordMessage = null;
public ?array $communityMessage = null;
public ?array $avatarMessage = null;
public ?array $premiumborderMessage = null;
// form fields
public int $modelrace = 0;
public int $modelgender = 0;
public int $idsInLists = 0;
public string $curEmail = '';
public string $curName = '';
public string $renameCD = '';
public string $activeCD = '';
public array $description = [];
public array $signature = [];
public int $avMode = 0;
public string $wowicon = '';
public int $customicon = 0;
public array $customicons = [];
public bool $premium = false;
public int $reputation = 0;
public ?Listview $avatarManager = null;
public ?array $bans;
public function __construct($pageParam)
{
if (!User::isLoggedIn())
$this->forwardToSignIn('account');
parent::__construct($pageParam);
}
protected function generate() : void
{
array_unshift($this->title, Lang::account('settings'));
$user = DB::Aowow()->selectRow('SELECT `debug`, `email`, `description`, `avatar`, `wowicon`, `renameCooldown` FROM ?_account WHERE `id` = ?d', User::$id);
Lang::sort('game', 'ra');
parent::generate();
/*************/
/* Ban Popup */
/*************/
$b = DB::Aowow()->select(
'SELECT ab.`end` AS "0", ab.`reason` AS "1", a.`username` AS "2"
FROM ?_account_banned ab
LEFT JOIN ?_account a ON a.`id` = ab.`staffId`
WHERE ab.`userId` = ?d AND ab.`typeMask` & ?d AND (ab.`end` = 0 OR ab.`end` > UNIX_TIMESTAMP())',
User::$id, ACC_BAN_TEMP | ACC_BAN_PERM
);
$this->bans = $b ?: null;
/*******************/
/* Status Messages */
/*******************/
if (isset($_SESSION['msg']))
{
[$var, $status, $msg] = $_SESSION['msg'];
if (property_exists($this, $var.'Message'))
$this->{$var.'Message'} = [$status, $msg];
else
trigger_error('AccountBaseResponse::generate - unknown var in $_SESSION msg: '.$var, E_USER_WARNING);
unset($_SESSION['msg']);
}
/*************/
/* Form Data */
/*************/
/* GENERAL */
// Modelviewer
if ($_ = DB::Aowow()->selectCell('SELECT `data` FROM ?_account_cookies WHERE `name` = ? AND `userId` = ?d', 'default_3dmodel', User::$id))
[$this->modelrace, $this->modelgender] = explode(',', $_);
// Lists
$this->idsInLists = $user['debug'] ? 1 : 0;
/* PERSONAL */
// Email address
$this->curEmail = $user['email'] ?? '';
// Username
$this->curName = User::$username;
$this->renameCD = DateTime::formatTimeElapsedFloat(Cfg::get('ACC_RENAME_DECAY') * 1000);
if ($user['renameCooldown'] > time())
{
$locCode = implode('_', str_split(Lang::getLocale()->json(), 2)); // ._.
$this->activeCD = (new \IntlDateFormatter($locCode, pattern: Lang::main('dateFmtIntl')))->format($user['renameCooldown']);
}
/* COMMUNITY */
// Public Description
$this->description = ['body' => $user['description']];
// Forum Signature
// $this->signature = ['body' => $user['signature']];
// Avatar
$this->wowicon = $user['wowicon'];
$this->avMode = $user['avatar'];
// status [reviewing, ok, rejected]? (only 2: rejected processed in js)
if (User::isPremium() && ($cuAvatars = DB::Aowow()->select('SELECT `id`, `name`, `current`, `size`, `status`, `when` FROM ?_account_avatars WHERE `userId` = ?d', User::$id)))
{
array_walk($cuAvatars, function (&$x) {
$x['when'] *= 1000; // uploaded timestamp expected as msec for some reason
$x['caption'] = $x['name']; // only used for getVisibleText, duplicates name?
$x['type'] = 1; // always 1 ?, Dialog-popup doesn't work without it
});
foreach ($cuAvatars as $a)
if ($a['status'] != AvatarMgr::STATUS_REJECTED)
$this->customicons[$a['id']] = $a['name'];
// TODO - replace with array_find in PHP 8.4
if ($x = array_filter($cuAvatars, fn($x) => $x['current'] > 0 ))
$this->customicon = array_pop($x)['id'];
}
/* PREMIUM */
$this->premium = User::isPremium();
if (!$this->premium)
return;
$this->reputation = User::getReputation();
// Avatar Manager
$this->avatarManager = new Listview([
'template' => 'avatar',
'id' => 'avatar',
'name' => '$LANG.tab_avatars',
'parent' => 'avatar-manage',
'hideNav' => 1 | 2, // top | bottom
'data' => $cuAvatars ?? [],
'note' => Lang::account('avatarSlots', [count($this->customicons), Cfg::get('acc_max_avatar_uploads')])
]);
// Premium Border Selector
// solved by js
}
}
?>

View File

@@ -0,0 +1,73 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via activation email link
* empty page with status box
*/
class AccountActivateResponse extends TemplateResponse
{
protected string $template = 'text-page-generic';
protected string $pageName = 'activate';
protected array $expectedGET = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']]
);
private bool $success = false;
public function __construct()
{
parent::__construct();
if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
$this->generateError();
}
protected function generate() : void
{
$this->title[] = Lang::account('title');
$msg = $this->activate();
if ($this->success)
$this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'register', [2]), 'message' => $msg]];
else
{
$_SESSION['error']['activate'] = $msg;
$this->forward('?account=resend');
}
parent::generate();
}
private function activate() : string
{
if (!$this->assertGET('key'))
return Lang::main('intError');
if (DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE `status` IN (?a) AND `token` = ?', [ACC_STATUS_NONE, ACC_STATUS_NEW], $this->_get['key']))
{
// don't remove the token yet. It's needed on signin page.
DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `userGroups` = ?d WHERE `token` = ?', ACC_STATUS_NONE, U_GROUP_NONE, $this->_get['key']);
// fully apply block for further registration attempts from this ip
DB::Aowow()->query('REPLACE INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d + 1, UNIX_TIMESTAMP() + ?d)',
User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'));
$this->success = true;
return Lang::account('inputbox', 'message', 'accActivated', [$this->_get['key']]);
}
// grace period expired and other user claimed name
return Lang::main('intError');
}
}
?>

View File

@@ -0,0 +1,128 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// custom handler
class AccountConfirmdeleteResponse extends TemplateResponse
{
protected string $template = 'delete';
protected string $pageName = 'confirm-delete';
protected array $scripts = array(
[SC_CSS_FILE, 'css/delete.css'],
[SC_CSS_STRING, '[type="submit"] { margin: 0px 10px; }']
);
protected array $expectedGET = array(
'key' => [FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']]
);
protected array $expectedPOST = array(
'submit' => [FILTER_UNSAFE_RAW ],
'cancel' => [FILTER_UNSAFE_RAW ],
'confirm' => [FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ],
'key' => [FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']]
);
public bool $confirm = true; // just to select the correct localized brick
public string $username = '';
public string $deleteFormTarget = '?account=confirm-delete';
public ?array $inputbox = null;
public string $key = '';
private bool $success = false;
public function __construct(string $pageParam)
{
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
$this->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
array_unshift($this->title, Lang::account('accDelete'));
$this->username = User::$username;
parent::generate();
$msg = Lang::account('inputbox', 'error', 'purgeTokenUsed');
// display default confirm template
if ($this->assertGET('key') && DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP() AND `token` = ?', ACC_STATUS_PURGING, $this->_get['key']))
{
$this->key = $this->_get['key'];
return;
}
// perform action and display status
if ($this->assertPOST('key') && ($userId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP() AND `token` = ?', ACC_STATUS_PURGING, $this->_post['key'])))
{
if ($this->_post['cancel'])
$msg = $this->cancel($userId);
else if ($this->_post['submit'] && $this->_post['confirm'])
$msg = $this->purge($userId);
}
// throw error and display in status
$this->inputbox = ['inputbox-status', array(
'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'),
'message' => $this->success ? $msg : '',
'error' => $this->success ? '' : $msg
)];
}
private function cancel(int $userId) : string
{
if (DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "" WHERE `id` = ?d', ACC_STATUS_NONE, $userId))
{
$this->success = true;
return Lang::account('inputbox', 'message', 'deleteCancel');
}
return Lang::main('intError');
}
private function purge(int $userId) : string
{
// empty all user settings and cookies
DB::Aowow()->query('DELETE FROM ?_account_cookies WHERE `userId` = ?d', $userId);
DB::Aowow()->query('DELETE FROM ?_account_avatars WHERE `userId` = ?d', $userId);
DB::Aowow()->query('DELETE FROM ?_account_excludes WHERE `userId` = ?d', $userId);
DB::Aowow()->query('DELETE FROM ?_account_favorites WHERE `userId` = ?d', $userId);
DB::Aowow()->query('DELETE FROM ?_account_reputation WHERE `userId` = ?d', $userId);
DB::Aowow()->query('DELETE FROM ?_account_weightscales WHERE `userId` = ?d', $userId); // cascades to aowow_account_weightscale_data
// delete profiles, unlink chars
DB::Aowow()->query('DELETE pp FROM ?_profiler_profiles pp JOIN ?_account_profiles ap ON ap.`profileId` = pp.`id` WHERE ap.`accountId` = ?d', $userId);
// DB::Aowow()->query('DELETE FROM ?_account_profiles WHERE `accountId` = ?d', $userId); // already deleted via FK?
// delete all sessions and bans
DB::Aowow()->query('DELETE FROM ?_account_banned WHERE `userId` = ?d', $userId);
DB::Aowow()->query('DELETE FROM ?_account_sessions WHERE `userId` = ?d', $userId);
// delete forum posts (msg: This post was from a user who has deleted their account. (no translations at src); comments/replies are unaffected)
// ...
// replace username with userId and empty fields
DB::Aowow()->query(
'UPDATE ?_account SET
`login` = "", `passHash` = "", `username` = `id`, `email` = NULL, `userGroups` = 0, `userPerms` = 0,
`curIp` = "", `prevIp` = "", `curLogin` = 0, `prevLogin` = 0,
`locale` = 0, `debug` = 0, `avatar` = 0, `wowicon` = "", `title` = "", `description` = "", `excludeGroups` = 0,
`status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "", `renameCooldown` = 0
WHERE `id` = ?d',
ACC_STATUS_DELETED, $userId
);
$this->success = true;
return Lang::account('inputbox', 'message', 'deleteOk');
}
}
?>

View File

@@ -0,0 +1,62 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via confirmation email link
* write status to session and redirect to account settings
*/
// ?auth=email-change
class AccountConfirmemailaddressResponse extends TemplateResponse
{
protected string $template = 'text-page-generic';
protected string $pageName = 'confirm-email-address';
protected array $expectedGET = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']]
);
private bool $success = false;
protected function generate() : void
{
parent::generate();
if (User::isBanned())
return;
$msg = $this->change();
$this->inputbox = ['inputbox-status', array(
'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'),
'message' => $this->success ? $msg : '',
'error' => $this->success ? '' : $msg,
)];
}
// this should probably leave change info intact for revert
// todo - move personal settings changes to separate table
private function change() : string
{
if (!$this->assertGET('key'))
return Lang::main('intError');
$acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']);
if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_EMAIL || $acc['statusTimer'] < time())
return Lang::account('inputbox', 'error', 'mailTokenUsed');
// 0 changes == error
if (!DB::Aowow()->query('UPDATE ?_account SET `email` = `updateValue`, `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key']))
return Lang::main('intError');
$this->success = true;
return Lang::account('inputbox', 'message', 'mailChangeOk');
}
}
?>

View File

@@ -0,0 +1,60 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via confirmation email link
* write status to session and redirect to account settings
*/
// 2025 - no longer in use?
class AccountConfirmpasswordResponse extends TemplateResponse
{
protected string $template = 'text-page-generic';
protected string $pageName = 'confirm-password';
protected array $expectedGET = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']]
);
private bool $success = false;
protected function generate() : void
{
parent::generate();
if (User::isBanned())
return;
$msg = $this->confirm();
$this->inputbox = ['inputbox-status', array(
'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'),
'message' => $this->success ? $msg : '',
'error' => $this->success ? '' : $msg,
)];
}
private function confirm() : string
{
if (!$this->assertGET('key'))
return Lang::main('intError');
$acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']);
if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_PASS || $acc['statusTimer'] < time())
return Lang::account('inputbox', 'error', 'passTokenUsed');
// 0 changes == error
if (!DB::Aowow()->query('UPDATE ?_account SET `passHash` = `updateValue`, `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key']))
return Lang::main('intError');
$this->success = true;
return Lang::account('inputbox', 'message', 'passChangeOk');
}
}
?>

View File

@@ -0,0 +1,47 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via form button on user settings page
*/
class AccountDeleteiconResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected int $requiredUserGroup = U_GROUP_PREMIUM_PERMISSIONS;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_VALIDATE_INT]
);
/*
* response not evaluated
*/
protected function generate() : void
{
if (User::isBanned() || !$this->assertPOST('id'))
return;
// non-int > error
$selected = DB::Aowow()->selectCell('SELECT `current` FROM ?_account_avatars WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'], User::$id);
if ($selected === null || $selected === false)
return;
DB::Aowow()->query('DELETE FROM ?_account_avatars WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'], User::$id);
// if deleted avatar is also currently selected, unset
if ($selected)
DB::Aowow()->query('UPDATE ?_account SET `avatar` = 0 WHERE `id` = ?d', User::$id);
$path = sprintf('static/uploads/avatars/%d.jpg', $this->_post['id']);
if (!unlink($path))
trigger_error('AccountDeleteiconResponse - failed to delete file: '.$path, E_USER_ERROR);
}
}
?>

View File

@@ -0,0 +1,71 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via account settings link
* empty page with status box
*/
class AccountDeleteResponse extends TemplateResponse
{
protected bool $requiresLogin = true;
protected string $template = 'delete';
protected string $pageName = 'delete';
protected array $scripts = [[SC_CSS_FILE, 'css/delete.css']];
protected array $expectedPOST = array(
'proceed' => ['filter' => FILTER_UNSAFE_RAW]
);
public string $username = '';
public string $deleteFormTarget = '?account=delete';
public ?array $inputbox = null;
public function __construct(string $pageParam)
{
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
$this->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
array_unshift($this->title, Lang::account('accDelete'));
parent::generate();
$this->username = User::$username;
if ($this->_post['proceed'])
{
$error = false;
if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `status` NOT IN (?a) AND `statusTimer` > UNIX_TIMESTAMP() AND `id` = ?d', [ACC_STATUS_NEW, ACC_STATUS_NONE, ACC_STATUS_PURGING], User::$id))
{
$token = Util::createHash(40);
DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d, `token` = ? WHERE `id` = ?d',
ACC_STATUS_PURGING, Cfg::get('ACC_RECOVERY_DECAY'), $token, User::$id);
Util::sendMail(User::$email, 'delete-account', [$token, User::$email, User::$username]);
}
else
$error = true;
$this->inputbox = ['inputbox-status', array(
'head' => Lang::account('inputbox', 'head', $error ? 'error' : 'success'),
'message' => $error ? '' : Lang::account('inputbox', 'message', 'deleteAccSent', [User::$email]),
'error' => $error ? Lang::account('inputbox', 'error', 'isRecovering') : ''
)];
}
}
}
?>

View File

@@ -0,0 +1,76 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed from character profiles, when setting exclusions on collections
* always returns emptry string
*/
class AccountExcludeResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'mode' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]],
'reset' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]],
'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ],
'type' => ['filter' => FILTER_VALIDATE_INT ],
'groups' => ['filter' => FILTER_VALIDATE_INT ]
);
protected function generate() : void
{
if (User::isBanned())
return;
if ($this->_post['mode'] == 1) // directly set exludes
$this->excludeById();
else if ($this->_post['reset'] == 1) // defaults to unavailable
$this->resetExcludes();
else if ($this->_post['groups']) // exclude by group mask
$this->updateGroups();
}
private function excludeById() : void
{
if (!$this->assertPOST('type', 'id'))
return;
if ($validIds = Type::validateIds($this->_post['type'], $this->_post['id']))
{
// ready for some bullshit? here it comes!
// we don't get signaled whether an id should be added to or removed from either includes or excludes
// so we throw everything into one table and toggle the mode if its already in here
$includes = DB::Aowow()->selectCol('SELECT `typeId` FROM ?_profiler_excludes WHERE `type` = ?d AND `typeId` IN (?a)', $this->_post['type'], $validIds);
foreach ($validIds as $typeId)
DB::Aowow()->query('INSERT INTO ?_account_excludes (`userId`, `type`, `typeId`, `mode`) VALUES (?a) ON DUPLICATE KEY UPDATE `mode` = (`mode` ^ 0x3)',
[User::$id, $this->_post['type'], $typeId, in_array($typeId, $includes) ? 2 : 1]
);
}
else
trigger_error('AccountExcludeResponse::excludeById - validation failed [type: '.$this->_post['type'].', typeId: '.implode(',', $this->_post['id']).']', E_USER_NOTICE);
}
private function resetExcludes() : void
{
DB::Aowow()->query('DELETE FROM ?_account_excludes WHERE `userId` = ?d', User::$id);
DB::Aowow()->query('UPDATE ?_account SET `excludeGroups` = ?d WHERE `id` = ?d', PR_EXCLUDE_GROUP_UNAVAILABLE, User::$id);
}
private function updateGroups() : void
{
if ($this->assertPOST('groups')) // clamp to real groups
DB::Aowow()->query('UPDATE ?_account SET `excludeGroups` = ?d WHERE `id` = ?d', $this->_post['groups'] & PR_EXCLUDE_GROUP_ANY, User::$id);
}
}
?>

View File

@@ -0,0 +1,52 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed from db detail pages, when clicking on the fav star near the h1 element
* always returns emptry string
*/
class AccountFavoritesResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'add' => ['filter' => FILTER_VALIDATE_INT],
'remove' => ['filter' => FILTER_VALIDATE_INT],
'id' => ['filter' => FILTER_VALIDATE_INT],
// 'sessionKey' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] // usage of sessionKey omitted
);
protected function generate() : void
{
if (User::isBanned())
return;
if ($this->_post['remove'])
$this->removeFavorite();
else if ($this->_post['add'])
$this->addFavorite();
}
private function removeFavorite() : void
{
if ($this->assertPOST('id', 'remove'))
DB::Aowow()->query('DELETE FROM ?_account_favorites WHERE `userId` = ?d AND `type` = ?d AND `typeId` = ?d', User::$id, $this->_post['remove'], $this->_post['id']);
}
private function addFavorite() : void
{
if ($this->assertPOST('id', 'add') && Type::validateIds($this->_post['add'], $this->_post['id']))
DB::Aowow()->query('INSERT INTO ?_account_favorites (`userId`, `type`, `typeId`) VALUES (?d, ?d, ?d)', User::$id, $this->_post['add'], $this->_post['id']);
else
trigger_error('AccountFavoritesResponse::addFavorite() - failed to add [userId: '.User::$id.', type: '.$this->_post['add'].', typeId: '.$this->_post['id'], E_USER_NOTICE);
}
}
?>

View File

@@ -0,0 +1,101 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via links on signin form and from recovery email
*
* A) redirect to external page
* B) 1. click password reset link > display email form
* 2. submit email form > send mail with recovery link
* 3. click recovery link from mail > display password reset form
* 4. submit password reset form > update password
*/
class AccountforgotpasswordResponse extends TemplateResponse
{
use TrRecoveryHelper, TrGetNext;
protected string $template = 'text-page-generic';
protected string $pageName = 'forgot-password';
protected array $expectedPOST = array(
'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW]
);
private bool $success = false;
public function __construct(string $pageParam)
{
// don't redirect logged in users
// you can be forgetful AND logged in
if (Cfg::get('ACC_EXT_RECOVER_URL'))
$this->forward(Cfg::get('ACC_EXT_RECOVER_URL'));
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
$this->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
$this->title[] = Lang::account('title');
parent::generate();
$msg = $this->processMailForm();
if ($this->success)
$this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'recoverPass', [1.5]), 'message' => $msg]];
else
$this->inputbox = ['inputbox-form-email', array(
'head' => Lang::account('inputbox', 'head', 'recoverPass', [1]),
'error' => $msg,
'action' => '?account=forgot-password&next='.$this->getNext(),
'email' => $this->_post['email'] ?? ''
)];
}
private function processMailForm() : string
{
// no input yet. show clean email form
if (is_null($this->_post['email']))
return '';
// truncated due to validation fail
if (!$this->_post['email'])
return Lang::account('emailInvalid');
$timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_PASSWORD_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT'));
// on cooldown pretend we dont know the email address
if ($timeout && $timeout > time())
return Cfg::get('DEBUG') ? 'resend on cooldown: '.DateTime::formatTimeElapsed($timeout * 1000).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound');
// pretend recovery started
if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ?', $this->_post['email']))
{
// do not confirm or deny existence of email
$this->success = !Cfg::get('DEBUG');
return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'recovPassSent', [$this->_post['email']]);
}
// recovery actually started
if ($err = $this->startRecovery(ACC_STATUS_RECOVER_PASS, 'reset-password', $this->_post['email']))
return $err;
DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d',
User::$ip, IP_BAN_TYPE_PASSWORD_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK'));
$this->success = true;
return Lang::account('inputbox', 'message', 'recovPassSent', [$this->_post['email']]);
}
}
?>

View File

@@ -0,0 +1,100 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via link on signin form
*
* A) redirect to external page
* B) 1. click password reset link > display email form
* 2. submit email form > send mail with recovery link
* ( 3. click recovery link from mail to go to signin page (so not on this page) )
*/
class AccountforgotusernameResponse extends TemplateResponse
{
use TrRecoveryHelper;
protected string $template = 'text-page-generic';
protected string $pageName = 'forgot-username';
protected array $expectedPOST = array(
'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW]
);
private bool $success = false;
public function __construct(string $pageParam)
{
// if the user is looged in goto account dashboard
if (User::isLoggedIn())
$this->forward('?account');
if (Cfg::get('ACC_EXT_RECOVER_URL'))
$this->forward(Cfg::get('ACC_EXT_RECOVER_URL'));
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
$this->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
$this->title[] = Lang::account('title');
parent::generate();
$msg = $this->processMailForm();
if ($this->success)
$this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'recoverUser'), 'message' => $msg]];
else
$this->inputbox = ['inputbox-form-email', array(
'head' => Lang::account('inputbox', 'head', 'recoverUser'),
'error' => $msg,
'action' => '?account=forgot-username'
)];
}
private function processMailForm() : string
{
// no input yet. show empty form
if (is_null($this->_post['email']))
return '';
// truncated due to validation fail
if (!$this->_post['email'])
return Lang::account('emailInvalid');
$timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_USERNAME_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT'));
// on cooldown pretend we dont know the email address
if ($timeout && $timeout > time())
return Cfg::get('DEBUG') ? 'resend on cooldown: '.DateTime::formatTimeElapsed($timeout * 1000).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound');
// pretend recovery started
if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ?', $this->_post['email']))
{
// do not confirm or deny existence of email
$this->success = !Cfg::get('DEBUG');
return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'recovUserSent', [$this->_post['email']]);
}
// recovery actually started
if ($err = $this->startRecovery(ACC_STATUS_RECOVER_USER, 'recover-user', $this->_post['email']))
return $err;
DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d',
User::$ip, IP_BAN_TYPE_USERNAME_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK'));
$this->success = true;
return Lang::account('inputbox', 'message', 'recovUserSent', [$this->_post['email']]);
}
}
?>

View File

@@ -0,0 +1,108 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via form submit on user settings page
*/
class AccountForumavatarResponse extends TextResponse
{
protected ?string $redirectTo = '?account#community';
protected bool $requiresLogin = true;
// called via form submit
protected array $expectedPOST = array(
'avatar' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 2 ]],
'wowicon' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/' ]], // file name can have \W chars: inv_misc_fork&knife, achievement_dungeon_drak'tharon_heroic
'customicon' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1 ]]
);
// called via ajax
protected array $expectedGET = array(
'avatar' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 2, 'max_range' => 2]],
'customicon' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1 ]]
);
private bool $success = false;
protected function generate() : void
{
if (User::isBanned())
return;
$msg = match ($this->_post['avatar'] ?? $this->_get['avatar'])
{
0 => $this->unset(), // none
1 => $this->fromIcon(), // wow icon
2 => $this->fromUpload(!$this->_get['avatar']), // custom icon (premium feature)
default => Lang::main('genericError')
};
if ($msg)
$_SESSION['msg'] = ['avatar', $this->success, $msg];
}
private function unset() : string
{
$x = DB::Aowow()->query('UPDATE ?_account SET `avatar` = 0 WHERE `id` = ?d', User::$id);
if ($x === null || $x === false)
return Lang::main('genericError');
$this->success = true;
return Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess');
}
private function fromIcon() : string
{
if (!$this->assertPOST('wowicon'))
return Lang::main('intError');
$icon = strtolower(trim($this->_post['wowicon']));
if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_icons WHERE `name` = ?', $icon))
return Lang::account('updateMessage', 'avNotFound');
$x = DB::Aowow()->query('UPDATE ?_account SET `avatar` = 1, `wowicon` = ? WHERE `id` = ?d', strtolower($icon), User::$id);
if ($x === null || $x === false)
return Lang::main('genericError');
$this->success = true;
$msg = Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess');
if (($qty = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_account WHERE `wowicon` = ?', $icon)) > 1)
$msg .= ' '.Lang::account('updateMessage', 'avNthUser', [$qty]);
else
$msg .= ' '.Lang::account('updateMessage', 'av1stUser');
return $msg;
}
protected function fromUpload(bool $viaPOST) : string
{
if (!User::isPremium())
return Lang::main('genericError');
if (($viaPOST && !$this->assertPOST('customicon')) || (!$viaPOST && !$this->assertGET('customicon')))
return Lang::main('intError');
$customIcon = $this->_post['customicon'] ?? $this->_get['customicon'];
$x = DB::Aowow()->query('UPDATE ?_account_avatars SET `current` = IF(`id` = ?d, 1, 0) WHERE `userId` = ?d AND `status` <> ?d', $customIcon, User::$id, AvatarMgr::STATUS_REJECTED);
if (!is_int($x))
return Lang::main('genericError');
if (!is_int(DB::Aowow()->query('UPDATE ?_account SET `avatar` = 2 WHERE `id` = ?d', User::$id)))
return Lang::main('intError');
$this->success = true;
return Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess');
}
}
?>

View File

@@ -0,0 +1,41 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via form submit on user settings page
*/
class AccountPremiumborderResponse extends TextResponse
{
protected ?string $redirectTo = '?account#premium';
protected bool $requiresLogin = true;
protected int $requiredUserGroup = U_GROUP_PREMIUM_PERMISSIONS;
protected array $expectedPOST = array(
'avatarborder' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 4]],
);
protected function generate() : void
{
if (User::isBanned())
return;
if (!$this->assertPOST('avatarborder'))
return;
$x = DB::Aowow()->query('UPDATE ?_account SET `avatarborder` = ?d WHERE `id` = ?d', $this->_post['avatarborder'], User::$id);
if (!is_int($x))
$_SESSION['msg'] = ['premiumborder', false, Lang::main('genericError')];
else if (!$x)
$_SESSION['msg'] = ['premiumborder', true, Lang::account('updateMessage', 'avNoChange')];
else
$_SESSION['msg'] = ['premiumborder', true, Lang::account('updateMessage', 'avSuccess')];
}
}
?>

View File

@@ -0,0 +1,36 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via form button on user settings page
*/
class AccountRenameiconResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected int $requiredUserGroup = U_GROUP_PREMIUM_PERMISSIONS;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_VALIDATE_INT ],
'name' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' =>'/^[a-zA-Z][a-zA-Z0-9 ]{0,19}$/']]
);
/*
* response not evaluated
*/
protected function generate() : void
{
if (User::isBanned() || !$this->assertPOST('id', 'name'))
return;
// regexp same as in account.js
DB::Aowow()->query('UPDATE ?_account_avatars SET `name` = ? WHERE `id` = ?d AND `userId` = ?d', trim($this->_post['name']), $this->_post['id'], User::$id);
}
}
?>

View File

@@ -0,0 +1,52 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed after successful resend request
* empty page with status box
*/
class AccountResendsubmitResponse extends TemplateResponse
{
protected string $template = 'text-page-generic';
protected string $pageName = 'resend';
protected array $expectedPOST = array(
'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW]
);
public function __construct(string $pageParam)
{
if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
$this->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
$this->title[] = Lang::account('title');
$error = $message = '';
if ($this->assertPOST('email'))
$message = Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]);
else
$error = Lang::main('intError');
parent::generate();
$this->inputbox = ['inputbox-status', array(
'head' => Lang::account('inputbox', 'head', 'register', [1.5]),
'message' => $message,
'error' => $error
)];
}
}
?>

View File

@@ -0,0 +1,98 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via link on login page
* empty page with status box
*/
class AccountResendResponse extends TemplateResponse
{
protected string $template = 'text-page-generic';
protected string $pageName = 'resend';
protected array $expectedPOST = array(
'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW]
);
private bool $success = false;
public function __construct(string $pageParam)
{
if (Cfg::get('ACC_EXT_RECOVER_URL'))
$this->forward(Cfg::get('ACC_EXT_RECOVER_URL'));
if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
$this->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
$this->title[] = Lang::account('title');
parent::generate();
// error from account=activate
if (isset($_SESSION['error']['activate']))
{
$msg = $_SESSION['error']['activate'];
unset($_SESSION['error']['activate']);
}
else
$msg = $this->resend();
if ($this->success)
$this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'resendMail'), 'message' => $msg]];
else
$this->inputbox = ['inputbox-form-email', array(
'head' => Lang::account('inputbox', 'head', 'resendMail'),
'message' => Lang::account('inputbox', 'message', 'resendMail'),
'error' => $msg,
'action' => '?account=resend',
)];
}
private function resend() : string
{
// no input yet. show clean form
if (is_null($this->_post['email']))
return '';
// truncated due to validation fail
if (!$this->_post['email'])
return Lang::account('emailInvalid');
$timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT'));
// on cooldown pretend we dont know the email address
if ($timeout && $timeout > time())
return Cfg::get('DEBUG') ? 'resend on cooldown: '.DateTime::formatTimeElapsed($timeout * 1000).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound');
// check email and account status
if ($token = DB::Aowow()->selectCell('SELECT `token` FROM ?_account WHERE `email` = ? AND `status` = ?d', $this->_post['email'], ACC_STATUS_NEW))
{
if (!Util::sendMail($this->_post['email'], 'activate-account', [$token]))
return Lang::main('intError');
DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d',
User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK'));
$this->success = true;
return Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]);
}
// pretend recovery started
// do not confirm or deny existence of email
$this->success = !Cfg::get('DEBUG');
return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]);
}
}
?>

View File

@@ -0,0 +1,121 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via links on signin form and from recovery email
*
* A) redirect to external page
* B) 1. click password reset link > display email form
* 2. submit email form > send mail with recovery link
* 3. click recovery link from mail > display password reset form
* 4. submit password reset form > update password
*/
class AccountresetpasswordResponse extends TemplateResponse
{
use TrRecoveryHelper, TrGetNext;
protected string $template = 'text-page-generic';
protected string $pageName = 'reset-password';
protected array $expectedGET = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']],
'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/' ]]
);
protected array $expectedPOST = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']],
'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW ],
'password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ],
'c_password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ]
);
private bool $success = false;
public function __construct()
{
$this->title[] = Lang::account('title');
parent::__construct();
// don't redirect logged in users
// you can be forgetful AND logged in
if (Cfg::get('ACC_EXT_RECOVER_URL'))
$this->forward(Cfg::get('ACC_EXT_RECOVER_URL'));
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
$this->generateError();
}
protected function generate() : void
{
parent::generate();
$errMsg = '';
if (!$this->assertGET('key') && !$this->assertPOST('key'))
$errMsg = Lang::account('inputbox', 'error', 'passTokenLost');
else if ($this->_get['key'] && !DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `token` = ? AND `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()', $this->_get['key'], ACC_STATUS_RECOVER_PASS))
$errMsg = Lang::account('inputbox', 'error', 'passTokenUsed');
if ($errMsg)
{
$this->inputbox = ['inputbox-status', array(
'head' => Lang::account('inputbox', 'head', 'error'),
'error' => $errMsg
)];
return;
}
// step "2.5"
$errMsg = $this->doResetPass();
if ($this->success)
$this->forward('?account=signin');
// step 2
$this->inputbox = ['inputbox-form-password', array(
'head' => Lang::account('inputbox', 'head', 'recoverPass', [2]),
'token' => $this->_post['key'] ?? $this->_get['key'],
'action' => '?account=reset-password&next=account=signin',
'error' => $errMsg,
)];
}
private function doResetPass() : string
{
// no input yet. show clean form
if (!$this->assertPOST('key', 'password', 'c_password') && is_null($this->_post['email']))
return '';
// truncated due to validation fail
if (!$this->_post['email'])
return Lang::account('emailInvalid');
if ($this->_post['password'] != $this->_post['c_password'])
return Lang::account('passCheckFail');
$userData = DB::Aowow()->selectRow('SELECT `id`, `passHash` FROM ?_account WHERE `token` = ? AND `email` = ? AND `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()',
$this->_post['key'],
$this->_post['email'],
ACC_STATUS_RECOVER_PASS
);
if (!$userData)
return Lang::account('inputbox', 'error', 'emailNotFound');
if (!User::verifyCrypt($this->_post['c_password'], $userData['passHash']))
return Lang::account('newPassDiff');
if (!DB::Aowow()->query('UPDATE ?_account SET `passHash` = ?, `status` = ?d WHERE `id` = ?d', User::hashCrypt($this->_post['c_password']), ACC_STATUS_NONE, $userData['id']))
return Lang::main('intError');
$this->success = true;
return '';
}
}
?>

View File

@@ -0,0 +1,62 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via revert email link
* write status to session and redirect to account settings
*/
// ?auth=email-revert
class AccountRevertemailaddressResponse extends TemplateResponse
{
protected string $template = 'text-page-generic';
protected string $pageName = 'revert-email-address';
protected array $expectedGET = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']]
);
private bool $success = false;
protected function generate() : void
{
parent::generate();
if (User::isBanned())
return;
$msg = $this->revert();
$this->inputbox = ['inputbox-status', array(
'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'),
'message' => $this->success ? $msg : '',
'error' => $this->success ? '' : $msg,
)];
}
// this should probably take precedence over email-change
// todo - move personal settings changes to separate table
private function revert() : string
{
if (!$this->assertGET('key'))
return Lang::main('intError');
$acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']);
if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_EMAIL || $acc['statusTimer'] < time())
return Lang::account('inputbox', 'error', 'mailTokenUsed');
// 0 changes == error
if (!DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key']))
return Lang::main('intError');
$this->success = true;
return Lang::account('inputbox', 'message', 'mailRevertOk');
}
}
?>

View File

@@ -0,0 +1,148 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
2 modes
A) show form
B) execute login and forward to
* self on failure
* next on success
*/
class AccountSigninResponse extends TemplateResponse
{
use TrGetNext;
protected string $template = 'text-page-generic';
protected string $pageName = 'signin';
protected array $expectedPOST = array(
'username' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateLogin'] ],
'password' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validatePassword']],
'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkRememberMe'] ]
);
protected array $expectedGET = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']],
'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/'] ]
);
private bool $success = false;
public function __construct()
{
// if the user is logged in, goto user dashboard
if (User::isLoggedIn())
$this->forward('?user='.User::$username);
parent::__construct();
}
protected function generate() : void
{
$username =
$error = '';
$rememberMe = !!$this->_post['remember_me'];
$this->title = [Lang::account('title')];
// coming from user recovery or creation, prefill username
if ($this->_get['key'])
{
if ($userData = DB::Aowow()->selectRow('SELECT a.`login` AS "0", IF(s.`expires`, 0, 1) AS "1" FROM ?_account a LEFT JOIN ?_account_sessions s ON a.`id` = s.`userId` AND a.`token` = s.`sessionId` WHERE a.`status` IN (?a) AND a.`token` = ?',
[ACC_STATUS_RECOVER_USER, ACC_STATUS_NONE], $this->_get['key']))
[$username, $rememberMe] = $userData;
}
if ($this->doSignIn($error))
$this->forward($this->getNext(true));
if ($error)
User::destroy();
$this->inputbox = ['inputbox-form-signin', array(
'head' => Lang::account('inputbox', 'head', 'signin'),
'action' => '?account=signin&next='.$this->getNext(),
'error' => $error,
'username' => $username,
'rememberMe' => $rememberMe,
'hasRecovery' => Cfg::get('ACC_EXT_RECOVER_URL') || Cfg::get('ACC_AUTH_MODE') == AUTH_MODE_SELF,
)];
parent::generate();
}
private function doSignIn(string &$error) : bool
{
if (is_null($this->_post['username']) && is_null($this->_post['password']))
return false;
if (!$this->assertPOST('username'))
{
$error = Lang::account('userNotFound');
return false;
}
if (!$this->assertPOST('password'))
{
$error = Lang::account('wrongPass');
return false;
}
$error = match (User::authenticate($this->_post['username'], $this->_post['password']))
{
AUTH_OK, AUTH_BANNED => $this->onAuthSuccess(),
// AUTH_BANNED => Lang::account('accBanned'); // ToDo: should this return an error? the actual account functionality should be blocked elsewhere
AUTH_WRONGUSER => Lang::account('userNotFound'),
AUTH_WRONGPASS => Lang::account('wrongPass'),
AUTH_IPBANNED => Lang::account('inputbox', 'error', 'loginExceeded', [DateTime::formatTimeElapsedFloat(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]),
AUTH_INTERNAL_ERR => Lang::main('intError'),
default => Lang::main('intError')
};
return !$error;
}
private function onAuthSuccess() : string
{
if (!User::$ip)
{
trigger_error('AccountSigninResponse::onAuthSuccess() - tried to login user without ip set', E_USER_ERROR);
return Lang::main('intError');
}
// reset account status, update expiration
$ok = DB::Aowow()->query('UPDATE ?_account SET `prevIP` = IF(`curIp` = ?, `prevIP`, `curIP`), `curIP` = IF(`curIp` = ?, `curIP`, ?), `status` = IF(`status` = ?d, `status`, 0), `statusTimer` = IF(`status` = ?d, `statusTimer`, 0), `token` = IF(`status` = ?d, `token`, "") WHERE `id` = ?d',
User::$ip, User::$ip, User::$ip,
ACC_STATUS_NEW, ACC_STATUS_NEW, ACC_STATUS_NEW,
User::$id // available after successful User:authenticate
);
if (!is_int($ok)) // num updated fields or null on fail
{
trigger_error('AccountSigninResponse::onAuthSuccess() - failed to update account status', E_USER_ERROR);
return Lang::main('intError');
}
// DELETE temp session
if ($this->_get['key'])
DB::Aowow()->query('DELETE FROM ?_account_sessions WHERE `sessionId` = ?', $this->_get['key']);
session_regenerate_id(true); // user status changed => regenerate id
// create new session entry
DB::Aowow()->query('INSERT INTO ?_account_sessions (`userId`, `sessionId`, `created`, `expires`, `touched`, `deviceInfo`, `ip`, `status`) VALUES (?d, ?, ?d, ?d, ?d, ?, ?, ?d)',
User::$id, session_id(), time(), $this->_post['remember_me'] ? 0 : time() + Cfg::get('SESSION_TIMEOUT_DELAY'), time(), User::$agent, User::$ip, SESSION_ACTIVE);
if (User::init()) // reinitialize the user
User::save();
return '';
}
}
?>

View File

@@ -0,0 +1,40 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AccountSignoutResponse extends TextResponse
{
use TrGetNext;
protected array $expectedGET = array(
'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/']],
'global' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ]
);
public function __construct(string $pageParam)
{
// if the user not is logged in goto login page
if (!User::isLoggedIn())
$this->forwardToSignIn();
parent::__construct($pageParam);
}
protected function generate() : void
{
if ($this->_get['global'])
DB::Aowow()->query('UPDATE ?_account_sessions SET `touched` = ?d, `status` = ?d WHERE `userId` = ?d', time(), SESSION_FORCED_LOGOUT, User::$id);
else
DB::Aowow()->query('UPDATE ?_account_sessions SET `touched` = ?d, `status` = ?d WHERE `sessionId` = ?', time(), SESSION_LOGOUT, session_id());
User::destroy();
$this->redirectTo = $this->getNext(true);
}
}
?>

View File

@@ -0,0 +1,163 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via signup link
* self referencing
*/
class AccountSignupResponse extends TemplateResponse
{
use TrGetNext;
protected string $template = 'text-page-generic';
protected string $pageName = 'signup';
protected array $expectedPOST = array(
'username' => ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW ],
'email' => ['filter' => FILTER_SANITIZE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW ],
'password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ],
'c_password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ],
'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkRememberMe']]
);
protected array $expectedGET = array(
'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/']]
);
private bool $success = false;
public function __construct()
{
// if the user is logged in goto account dashboard
if (User::isLoggedIn())
$this->forward('?account');
// redirect to external registration page, if set
if (Cfg::get('ACC_EXT_CREATE_URL'))
$this->forward(Cfg::get('ACC_EXT_CREATE_URL'));
parent::__construct();
// registration not enabled on self
if (!Cfg::get('ACC_ALLOW_REGISTER'))
$this->generateError();
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
$this->generateError();
}
protected function generate() : void
{
$this->title[] = Lang::account('title');
// step 1 - no params > signup form
// step 2 - any param > status box
// step 3 - on ?account=activate
$message = $this->doSignUp();
if ($this->success)
{
$this->inputbox = ['inputbox-status', array(
'head' => Lang::account('inputbox', 'head', 'register', [1.5]),
'message' => Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']])
)];
}
else
{
$this->inputbox = ['inputbox-form-signup', array(
'head' => Lang::account('inputbox', 'head', 'register', [1]),
'error' => $message,
'action' => '?account=signup&next='.$this->getNext(),
'username' => $this->_post['username'] ?? '',
'email' => $this->_post['email'] ?? '',
'rememberMe' => !!$this->_post['remember_me'],
)];
}
parent::generate();
}
private function doSignUp() : string
{
// no input yet. show clean form
if (!$this->assertPOST('username', 'password', 'c_password') && is_null($this->_post['email']))
return '';
// truncated due to validation fail
if (!$this->_post['email'])
return Lang::account('emailInvalid');
// check username
if (!Util::validateUsername($this->_post['username'], $e))
return Lang::account($e == 1 ? 'errNameLength' : 'errNameChars');
// check password
if (!Util::validatePassword($this->_post['password'], $e))
return Lang::account($e == 1 ? 'errPassLength' : 'errPassChars');
if ($this->_post['password'] !== $this->_post['c_password'])
return Lang::account('passMismatch');
// check ip
if (!User::$ip)
return Lang::main('intError');
// limit account creation
if (DB::Aowow()->selectRow('SELECT 1 FROM ?_account_bannedips WHERE `type` = ?d AND `ip` = ? AND `count` >= ?d AND `unbanDate` >= UNIX_TIMESTAMP()', IP_BAN_TYPE_REGISTRATION_ATTEMPT, User::$ip, Cfg::get('ACC_FAILED_AUTH_COUNT')))
{
DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ? AND `type` = ?d', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT);
return Lang::account('inputbox', 'error', 'signupExceeded', [DateTime::formatTimeElapsedFloat(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]);
}
// username / email taken
if ($inUseData = DB::Aowow()->SelectRow('SELECT `id`, `username`, `status` = ?d AND `statusTimer` < UNIX_TIMESTAMP() AS "expired" FROM ?_account WHERE (LOWER(`username`) = LOWER(?) OR LOWER(`email`) = LOWER(?))', ACC_STATUS_NEW, $this->_post['username'], $this->_post['email']))
{
if ($inUseData['expired'])
DB::Aowow()->query('DELETE FROM ?_account WHERE `id` = ?d', $inUseData['id']);
else
return Util::lower($inUseData['username']) == Util::lower($this->_post['username']) ? Lang::account('nameInUse') : Lang::account('mailInUse');
}
// create..
$token = Util::createHash();
$userId = DB::Aowow()->query('INSERT INTO ?_account (`login`, `passHash`, `username`, `email`, `joindate`, `curIP`, `locale`, `userGroups`, `status`, `statusTimer`, `token`) VALUES (?, ?, ?, ?, UNIX_TIMESTAMP(), ?, ?d, ?d, ?d, UNIX_TIMESTAMP() + ?d, ?)',
$this->_post['username'],
User::hashCrypt($this->_post['password']),
$this->_post['username'],
$this->_post['email'],
User::$ip,
Lang::getLocale()->value,
U_GROUP_PENDING,
ACC_STATUS_NEW,
Cfg::get('ACC_CREATE_SAVE_DECAY'),
$token
);
if (!$userId)
return Lang::main('intError');
// create session tied to the token to store remember_me status
DB::Aowow()->query('INSERT INTO ?_account_sessions (`userId`, `sessionId`, `created`, `expires`, `touched`, `deviceInfo`, `ip`, `status`) VALUES (?d, ?, ?d, ?d, ?d, ?, ?, ?d)',
$userId, $token, time(), $this->_post['remember_me'] ? 0 : time() + Cfg::get('SESSION_TIMEOUT_DELAY'), time(), User::$agent, User::$ip, SESSION_ACTIVE);
if (!Util::sendMail($this->_post['email'], 'activate-account', [$token], Cfg::get('ACC_CREATE_SAVE_DECAY')))
return Lang::main('intError2', ['send mail']);
// success: update ip-bans
DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, 1, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d',
User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK'));
Util::gainSiteReputation($userId, SITEREP_ACTION_REGISTER);
$this->success = true;
return '';
}
}
?>

View File

@@ -0,0 +1,48 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via account settings form submit
* write status to session and redirect to account settings
*/
class AccountUpdatecommunitysettingsResponse extends TextResponse
{
protected ?string $redirectTo = '?account#community';
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'desc' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']]
);
private bool $success = false;
protected function generate() : void
{
if (User::isBanned())
return;
if ($message = $this->updateSettings())
$_SESSION['msg'] = ['community', $this->success, $message];
}
protected function updateSettings()
{
if (is_null($this->_post['desc'])) // assertPOST tests for empty string which is valid here
return Lang::main('genericError');
// description - 0 modified rows is still success
if (!is_int(DB::Aowow()->query('UPDATE ?_account SET `description` = ? WHERE `id` = ?d', $this->_post['desc'], User::$id)))
return Lang::main('genericError');
$this->success = true;
return Lang::account('updateMessage', 'community');
}
}
?>

View File

@@ -0,0 +1,80 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via account settings form submit
* write status to session and redirect to account settings
*/
class AccountUpdateemailResponse extends TextResponse
{
protected ?string $redirectTo = '?account#personal';
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'newemail' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW]
);
private bool $success = false;
public function __construct(string $pageParam)
{
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
(new TemplateResponse())->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
if (User::isBanned())
return;
if ($msg = $this->updateMail())
$_SESSION['msg'] = ['email', $this->success, $msg];
}
private function updateMail() : string
{
// no input yet
if (is_null($this->_post['newemail']))
return Lang::main('intError');
// truncated due to validation fail
if (!$this->_post['newemail'])
return Lang::account('emailInvalid');
if (DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ? AND `id` <> ?d', $this->_post['newemail'], User::$id))
return Lang::account('mailInUse');
$status = DB::Aowow()->selectCell('SELECT `status` FROM ?_account WHERE `statusTimer` > UNIX_TIMESTAMP() AND `id` = ?d', User::$id);
if ($status != ACC_STATUS_NONE && $status != ACC_STATUS_CHANGE_EMAIL)
return Lang::account('inputbox', 'error', 'isRecovering', [DateTime::formatTimeElapsedFloat(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]);
$oldEmail = DB::Aowow()->selectCell('SELECT `email` FROM ?_account WHERE `id` = ?d', User::$id);
if ($this->_post['newemail'] == $oldEmail)
return Lang::account('newMailDiff');
$token = Util::createHash();
// store new mail in updateValue field, exchange when confirmation mail gets confirmed
if (!DB::Aowow()->query('UPDATE ?_account SET `updateValue` = ?, `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d, `token` = ? WHERE `id` = ?d',
$this->_post['newemail'], ACC_STATUS_CHANGE_EMAIL, Cfg::get('ACC_RECOVERY_DECAY'), $token, User::$id))
return Lang::main('intError');
if (!Util::sendMail($this->_post['newemail'], 'change-email', [$token, $this->_post['newemail']], Cfg::get('ACC_RECOVERY_DECAY')))
return Lang::main('intError2', ['send mail']);
if (!Util::sendMail($oldEmail, 'revert-email', [$token, $oldEmail], Cfg::get('ACC_RECOVERY_DECAY')))
return Lang::main('intError2', ['send mail']);
$this->success = true;
return Lang::account('updateMessage', 'personal', [$this->_post['newemail']]);
}
}
?>

View File

@@ -0,0 +1,60 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via account settings form submit
* write status to session and redirect to account settings
*/
class AccountUpdategeneralsettingsResponse extends TextResponse
{
protected ?string $redirectTo = '?account#general';
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'modelrace' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['default' => 0, 'min_range' => 1, 'max_range' => 11]],
'modelgender' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['default' => 0, 'min_range' => 1, 'max_range' => 2] ],
'idsInLists' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCheckbox'] ]
);
private bool $success = false;
protected function generate() : void
{
if (User::isBanned())
return;
if ($message = $this->updateGeneral())
$_SESSION['msg'] = ['general', $this->success, $message];
}
private function updateGeneral() : string
{
if (!$this->assertPOST('modelrace', 'modelgender'))
return Lang::main('genericError');
if ($this->_post['modelrace'] && !ChrRace::tryFrom($this->_post['modelrace']))
return Lang::main('genericError');
// js handles this as cookie, so saved as cookie; Q - also save in ?_account table?
if (!DB::Aowow()->query('REPLACE INTO ?_account_cookies (`userId`, `name`, `data`) VALUES (?d, ?, ?)', User::$id, 'default_3dmodel', $this->_post['modelrace']. ',' . $this->_post['modelgender']))
return Lang::main('genericError');
if (!setcookie('default_3dmodel', $this->_post['modelrace']. ',' . $this->_post['modelgender'], 0, '/'))
return Lang::main('intError');
// int > number of edited rows > no changes is still success
if (!is_int(DB::Aowow()->query('UPDATE ?_account SET `debug` = ?d WHERE `id` = ?d', $this->_post['idsInLists'] ? 1 : 0, User::$id)))
return Lang::main('intError');
$this->success = true;
return Lang::account('updateMessage', 'general');
}
}
?>

View File

@@ -0,0 +1,86 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via account settings form submit
* write status to session and redirect to account settings
*/
class AccountUpdatepasswordResponse extends TextResponse
{
protected ?string $redirectTo = '?account#personal';
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'currentPassword' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']],
'newPassword' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']],
'confirmPassword' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']],
'globalLogout' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCheckbox']]
);
private bool $success = false;
public function __construct(string $pageParam)
{
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
(new TemplateResponse())->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
if (User::isBanned())
return;
if ($msg = $this->updatePassword())
$_SESSION['msg'] = ['password', $this->success, $msg];
}
private function updatePassword() : string
{
if (!$this->assertPOST('currentPassword', 'newPassword', 'confirmPassword'))
return Lang::main('intError');
if (!Util::validatePassword($this->_post['newPassword'], $e))
return Lang::account($e == 1 ? 'errPassLength' : 'errPassChars');
if ($this->_post['newPassword'] !== $this->_post['confirmPassword'])
return Lang::account('passMismatch');
$userData = DB::Aowow()->selectRow('SELECT `status`, `passHash`, `statusTimer` FROM ?_account WHERE `id` = ?d', User::$id);
if ($userData['status'] != ACC_STATUS_NONE && $userData['status'] != ACC_STATUS_CHANGE_PASS && $userData['statusTimer'] > time())
return Lang::account('inputbox', 'error', 'isRecovering', [DateTime::formatTimeElapsedFloat(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]);
if (!User::verifyCrypt($this->_post['currentPassword'], $userData['passHash']))
return Lang::account('wrongPass');
if (User::verifyCrypt($this->_post['newPassword'], $userData['passHash']))
return Lang::account('newPassDiff');
$token = Util::createHash();
// store new hash in updateValue field, exchange when confirmation mail gets confirmed
if (!DB::Aowow()->query('UPDATE ?_account SET `updateValue` = ?, `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d, `token` = ? WHERE `id` = ?d',
User::hashCrypt($this->_post['newPassword']), ACC_STATUS_CHANGE_PASS, Cfg::get('ACC_RECOVERY_DECAY'), $token, User::$id))
return Lang::main('intError');
$email = DB::Aowow()->selectCell('SELECT `email` FROM ?_account WHERE `id` = ?d', User::$id);
if (!Util::sendMail($email, 'update-password', [$token, $email], Cfg::get('ACC_RECOVERY_DECAY')))
return Lang::main('intError2', ['send mail']);
// logout all other active sessions
if ($this->_post['globalLogout'])
DB::Aowow()->query('UPDATE ?_account_sessions SET `status` = ?d, `touched` = ?d WHERE `userId` = ?d AND `sessionId` <> ? AND `status` = ?d', SESSION_FORCED_LOGOUT, time(), User::$id, session_id(), SESSION_ACTIVE);
$this->success = true;
return Lang::account('updateMessage', 'personal', [User::$email]);
}
}
?>

View File

@@ -0,0 +1,61 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via account settings form submit
* write status to session and redirect to account settings
*/
class AccountUpdateusernameResponse extends TextResponse
{
protected ?string $redirectTo = '?account#personal';
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'newUsername' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']]
);
private bool $success = false;
public function __construct(string $pageParam)
{
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
(new TemplateResponse())->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
if (User::isBanned())
return;
if ($msg = $this->updateUsername())
$_SESSION['msg'] = ['username', $this->success, $msg];
}
private function updateUsername() : string
{
if (!$this->assertPOST('newUsername'))
return Lang::main('intError');
if (DB::Aowow()->selectCell('SELECT `renameCooldown` FROM ?_account WHERE `id` = ?d', User::$id) > time())
return Lang::main('intError'); // should have grabbed the error response..
// yes, including your current name. you don't want to change into your current name, right?
if (DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_post['newUsername']))
return Lang::account('nameInUse');
DB::Aowow()->query('UPDATE ?_account SET `username` = ?, `renameCooldown` = ?d WHERE `id` = ?d', $this->_post['newUsername'], time() + Cfg::get('acc_rename_decay'), User::$id);
$this->success = true;
return Lang::account('updateMessage', 'username', [User::$username, $this->_post['newUsername']]);
}
}
?>

View File

@@ -0,0 +1,117 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via ajax
* returns scaleId if successful, 0 if not
*/
class AccountWeightscalesResponse extends TextResponse
{
private const /* int */ MAX_SCALES = 5; // more or less hard-defined in LANG.message_weightscalesaveerror
protected bool $requiresLogin = true;
protected mixed $result = 0; // default to error
protected array $expectedPOST = array(
'save' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]],
'delete' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]],
'id' => ['filter' => FILTER_VALIDATE_INT ],
'name' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkName'] ],
'scale' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkScale'] ]
);
protected function generate() : void
{
if (User::isBanned())
return;
if ($this->_post['save'] && $this->_post['id'])
$this->updateWeights();
else if ($this->_post['save'])
$this->createWeights();
else if ($this->_post['delete'])
$this->deleteWeights();
}
private function createWeights() : void
{
if (!$this->assertPOST('name', 'scale'))
return;
$nScales = DB::Aowow()->selectCell('SELECT COUNT(`id`) FROM ?_account_weightscales WHERE `userId` = ?d', User::$id);
if ($nScales >= self::MAX_SCALES)
return;
if ($id = DB::Aowow()->query('INSERT INTO ?_account_weightscales (`userId`, `name`) VALUES (?d, ?)', User::$id, $this->_post['name']))
if ($this->storeScaleData($id))
$this->result = $id;
}
private function updateWeights() : void
{
if (!$this->assertPOST('name', 'scale', 'id'))
return;
// not in DB or not owned by user
if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account_weightscales WHERE `userId` = ?d AND `id` = ?d', User::$id, $this->_post['id']))
{
trigger_error('AccountWeightscalesResponse::updateWeights - scale #'.$this->_post['id'].' not in db or not owned by user #'.User::$id, E_USER_ERROR);
return;
}
DB::Aowow()->query('UPDATE ?_account_weightscales SET `name` = ? WHERE `id` = ?d', $this->_post['name'], $this->_post['id']);
$this->storeScaleData($this->_post['id']);
// return edited id on success
$this->result = $this->_post['id'];
}
private function deleteWeights() : void
{
if ($this->assertPOST('id'))
DB::Aowow()->query('DELETE FROM ?_account_weightscales WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'], User::$id);
$this->result = '';
}
private function storeScaleData(int $scaleId) : bool
{
if (!is_int(DB::Aowow()->query('DELETE FROM ?_account_weightscale_data WHERE `id` = ?d', $scaleId)))
return false;
foreach ($this->_post['scale'] as [$k, $v])
if (in_array($k, Util::$weightScales)) // $v is known to be a positive int due to regex check
if (!is_int(DB::Aowow()->query('INSERT INTO ?_account_weightscale_data VALUES (?d, ?, ?d)', $scaleId, $k, $v)))
return false;
return true;
}
/*************************************/
/* additional request data callbacks */
/*************************************/
protected static function checkScale(string $val) : array
{
if (preg_match('/^((\w+:\d+)(,\w+:\d+)*)$/', $val))
return array_map(fn($x) => explode(':', $x), explode(',', $val));
return [];
}
protected static function checkName(string $val) : string
{
return mb_substr(preg_replace('/[^[:print:]]/', '', trim(urldecode($val))), 0, 32);
}
}
?>

View File

@@ -0,0 +1,511 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/* Notes:
* can create achievement progress bars with
* g_createProgressBar(c)
* var c = {
* text: "",
* hoverText: "",
* color: "", // cssClassName rep[0-7] | ach[0|1]
* width: 0, // 0 <=> 100
* }
*/
class AchievementBaseResponse extends TemplateResponse implements ICache
{
use TrDetailPage, TrCache;
protected int $cacheType = CACHE_TYPE_DETAIL_PAGE;
protected string $template = 'achievement';
protected string $pageName = 'achievement';
protected ?int $activeTab = parent::TAB_DATABASE;
protected array $breadcrumb = [0, 9];
public int $type = Type::ACHIEVEMENT;
public int $typeId = 0;
public int $reqCrtQty = 0;
public ?array $mail = null;
public string $description = '';
public array $criteria = [];
public ?array $rewards = null;
private AchievementList $subject;
public function __construct(string $id)
{
parent::__construct($id);
$this->typeId = intVal($id);
$this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE;
}
protected function generate() : void
{
$this->subject = new AchievementList(array(['id', $this->typeId]));
if ($this->subject->error)
$this->generateNotFound(Lang::game('achievement'), Lang::achievement('notFound'));
$this->extendGlobalData($this->subject->getJSGlobals(GLOBALINFO_REWARDS));
$this->h1 = $this->subject->getField('name', true);
$this->gPageInfo += array(
'type' => $this->type,
'typeId' => $this->typeId,
'name' => $this->h1
);
/*************/
/* Menu Path */
/*************/
// create page title and path
$curCat = $this->subject->getField('category');
$catPath = [];
while ($curCat > 0)
{
$catPath[] = $curCat;
$curCat = DB::Aowow()->SelectCell('SELECT `parentCat` FROM ?_achievementcategory WHERE `id` = ?d', $curCat);
}
$this->breadcrumb = array_merge($this->breadcrumb, array_reverse($catPath));
/**************/
/* Page Title */
/**************/
array_unshift($this->title, $this->subject->getField('name', true), Util::ucFirst(Lang::game('achievement')));
/***********/
/* Infobox */
/***********/
$infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags'));
// points
if ($_ = $this->subject->getField('points'))
$infobox[] = Lang::achievement('points').Lang::main('colon').'[achievementpoints='.$_.']';
// location
// todo (low)
// faction
$infobox[] = Lang::main('side') . match ($this->subject->getField('faction'))
{
SIDE_ALLIANCE => '[span class=icon-alliance]'.Lang::game('si', SIDE_ALLIANCE).'[/span]',
SIDE_HORDE => '[span class=icon-horde]'.Lang::game('si', SIDE_HORDE).'[/span]',
default => Lang::game('si', SIDE_BOTH) // 0, 3
};
// id
$infobox[] = Lang::achievement('id') . $this->typeId;
// icon
if ($_ = $this->subject->getField('iconId'))
{
$infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]';
$this->extendGlobalIds(Type::ICON, $_);
}
// profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view)
if (Cfg::get('PROFILER_ENABLE') && !($this->subject->getField('flags') & ACHIEVEMENT_FLAG_COUNTER))
{
$x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_achievements WHERE `achievementId` = ?d', $this->typeId);
$y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `custom` = 0 AND `stub` = 0');
$infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]);
// completion row added by InfoboxMarkup
}
// original name
if (Lang::getLocale() != Locale::EN)
$infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]';
if ($infobox)
$this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0', !($this->subject->getField('flags') & ACHIEVEMENT_FLAG_COUNTER));
/**********/
/* Series */
/**********/
$series = [];
if ($c = $this->subject->getField('chainId'))
{
$chainAcv = new AchievementList(array(['chainId', $c]));
foreach ($chainAcv->iterate() as $aId => $__)
{
$pos = $chainAcv->getField('chainPos');
if (!isset($series[$pos]))
$series[$pos] = [];
$series[$pos][] = array(
'side' => (int)$chainAcv->getField('faction'),
'typeStr' => Type::getFileString(Type::ACHIEVEMENT),
'typeId' => $aId,
'name' => $chainAcv->getField('name', true)
);
}
}
if ($series)
$this->series = [[array_values($series), null]];
/****************/
/* Main Content */
/****************/
$this->headIcons = [$this->subject->getField('iconString')];
$this->description = $this->subject->getField('description', true);
$this->redButtons = array(
BUTTON_WOWHEAD => !($this->subject->getField('cuFlags') & CUSTOM_SERVERSIDE),
BUTTON_LINKS => array(
'linkColor' => 'ffffff00',
'linkId' => Type::getFileString(Type::ACHIEVEMENT).':'.$this->typeId.':&quot;..UnitGUID(&quot;player&quot;)..&quot;:0:0:0:0:0:0:0:0',
'linkName' => $this->h1,
'type' => $this->type,
'typeId' => $this->typeId
)
);
$this->reqCrtQty = $this->subject->getField('reqCriteriaCount');
if ($this->createMail())
$this->addScript([SC_CSS_FILE, 'css/Book.css']);
// create rewards
$rewItems = $rewTitles = [];
if ($foo = $this->subject->getField('rewards'))
{
if ($itemRewards = array_filter($foo, fn($x) => $x[0] == Type::ITEM))
{
$bar = new ItemList(array(['i.id', array_column($itemRewards, 1)]));
foreach ($bar->iterate() as $id => $__)
$rewItems[] = new IconElement(Type::ITEM, $id, $bar->getField('name', true), quality: $bar->getField('quality'));
}
if ($titleRewards = array_filter($foo, fn($x) => $x[0] == Type::TITLE))
{
$bar = new TitleList(array(['id', array_column($titleRewards, 1)]));
foreach ($bar->iterate() as $id => $__)
$rewTitles[] = Lang::achievement('titleReward', [$id, trim(str_replace('%s', '', $bar->getField('male', true)))]);
}
}
if (($text = $this->subject->getField('reward', true)) || $rewItems || $rewTitles)
$this->rewards = [$rewItems, $rewTitles, $text];
// factionchange-equivalent
if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = ?d, `alliance_id`, -`horde_id`) FROM player_factionchange_achievement WHERE `alliance_id` = ?d OR `horde_id` = ?d', $this->typeId, $this->typeId, $this->typeId))
{
$altAcv = new AchievementList(array(['id', abs($pendant)]));
if (!$altAcv->error)
{
$this->transfer = Lang::achievement('_transfer', array(
$altAcv->id,
ITEM_QUALITY_NORMAL,
$altAcv->getField('iconString'),
$altAcv->getField('name', true),
$pendant > 0 ? 'alliance' : 'horde',
$pendant > 0 ? Lang::game('si', SIDE_ALLIANCE) : Lang::game('si', SIDE_HORDE)
));
}
}
/*****************/
/* Criteria List */
/*****************/
// serverside extra-Data (not sure why ACHIEVEMENT_CRITERIA_DATA_TYPE_NONE is set, let a lone a couple hundred times)
if ($crtIds = array_column($this->subject->getCriteria(), 'id'))
$crtExtraData = DB::World()->select('SELECT `criteria_id` AS ARRAY_KEY, `type` AS ARRAY_KEY2, `value1`, `value2`, `ScriptName` FROM achievement_criteria_data WHERE `type` <> ?d AND `criteria_id` IN (?a)', ACHIEVEMENT_CRITERIA_DATA_TYPE_NONE, $crtIds);
else
$crtExtraData = [];
foreach ($this->subject->getCriteria() as $crt)
{
// hide hidden criteria for regular users (really do..?)
// if (($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_HIDDEN) && !User::isInGroup(U_GROUP_STAFF))
// continue;
// alternative display option
$crtName = Util::localizedString($crt, 'name');
$killSuffix = null;
$obj = (int)$crt['value1'];
$qty = (int)$crt['value2'];
switch ($crt['type'])
{
// link to npc
case ACHIEVEMENT_CRITERIA_TYPE_KILL_CREATURE:
$killSuffix = Lang::achievement('slain');
case ACHIEVEMENT_CRITERIA_TYPE_KILLED_BY_CREATURE:
$crtIcon = new IconElement(Type::NPC, $obj, $crtName ?: CreatureList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon', extraText: $crtName ? null : $killSuffix);
break;
// link to area (by map)
case ACHIEVEMENT_CRITERIA_TYPE_WIN_BG:
case ACHIEVEMENT_CRITERIA_TYPE_WIN_ARENA:
case ACHIEVEMENT_CRITERIA_TYPE_PLAY_ARENA:
case ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_BATTLEGROUND:
case ACHIEVEMENT_CRITERIA_TYPE_DEATH_AT_MAP:
$zoneId = DB::Aowow()->selectCell('SELECT `id` FROM ?_zones WHERE `mapId` = ?', $obj);
$crtIcon = new IconElement(Type::ZONE, $zoneId ?: 0, $crtName ?: ZoneList::getName($zoneId), size: IconElement::SIZE_SMALL, element: 'iconlist-icon');
break;
// link to area
case ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUESTS_IN_ZONE:
case ACHIEVEMENT_CRITERIA_TYPE_HONORABLE_KILL_AT_AREA:
$crtIcon = new IconElement(Type::ZONE, $obj, $crtName ?: ZoneList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon');
break;
// link to skills
case ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL:
case ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LEVEL:
case ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILLLINE_SPELLS:
case ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LINE:
$crtIcon = new IconElement(Type::SKILL, $obj, $crtName ?: SkillList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon');
$this->extendGlobalIds(Type::SKILL, $obj);
break;
// link to class
case ACHIEVEMENT_CRITERIA_TYPE_HK_CLASS:
$crtIcon = new IconElement(Type::CHR_CLASS, $obj, $crtName ?: CharClassList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon');
break;
// link to race
case ACHIEVEMENT_CRITERIA_TYPE_HK_RACE:
$crtIcon = new IconElement(Type::CHR_RACE, $obj, $crtName ?: CharRaceList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon');
break;
// link to achivement
case ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_ACHIEVEMENT:
$crtIcon = new IconElement(Type::ACHIEVEMENT, $obj, $crtName ?: AchievementList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon');
$this->extendGlobalIds(Type::ACHIEVEMENT, $obj);
break;
// link to quest
case ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUEST:
$crtIcon = new IconElement(Type::QUEST, $obj, $crtName ?: QuestList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon');
break;
// link to spell
case ACHIEVEMENT_CRITERIA_TYPE_BE_SPELL_TARGET:
case ACHIEVEMENT_CRITERIA_TYPE_BE_SPELL_TARGET2:
case ACHIEVEMENT_CRITERIA_TYPE_CAST_SPELL:
case ACHIEVEMENT_CRITERIA_TYPE_LEARN_SPELL:
case ACHIEVEMENT_CRITERIA_TYPE_CAST_SPELL2:
$crtIcon = new IconElement(Type::SPELL, $obj, $crtName ?: SpellList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon');
$this->extendGlobalIds(Type::SPELL, $obj);
break;
// link to item
case ACHIEVEMENT_CRITERIA_TYPE_OWN_ITEM:
case ACHIEVEMENT_CRITERIA_TYPE_USE_ITEM:
case ACHIEVEMENT_CRITERIA_TYPE_LOOT_ITEM:
case ACHIEVEMENT_CRITERIA_TYPE_EQUIP_ITEM:
$item = new ItemList([['id', $obj]]);
$crtIcon = new IconElement(Type::ITEM, $obj, $crtName ?: $item->getField('name', true), quality: $item->getField('quality'), size: IconElement::SIZE_SMALL, element: 'iconlist-icon');
$this->extendGlobalData($item->getJSGlobals());
break;
// link to faction (/w target reputation)
case ACHIEVEMENT_CRITERIA_TYPE_GAIN_REPUTATION:
$crtIcon = new IconElement(Type::FACTION, $obj, $crtName ?: FactionList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon', extraText: '('.Lang::getReputationLevelForPoints($qty).')');
break;
// link to GObject
case ACHIEVEMENT_CRITERIA_TYPE_USE_GAMEOBJECT:
case ACHIEVEMENT_CRITERIA_TYPE_FISH_IN_GAMEOBJECT:
$crtIcon = new IconElement(Type::OBJECT, $obj, $crtName ?: GameObjectList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon');
break;
// link to emote
case ACHIEVEMENT_CRITERIA_TYPE_DO_EMOTE:
$crtIcon = new IconElement(Type::EMOTE, $obj, $crtName ?: EmoteList::getName($obj), size: IconElement::SIZE_SMALL, element: 'iconlist-icon');
break;
default:
// Add a gold coin icon if required
if ($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_MONEY_COUNTER )
$crtIcon = new IconElement(0, 0, '', extraText: Util::formatMoney($qty));
else
$crtIcon = new IconElement(0, 0, $crtName);
break;
}
if (User::isInGroup(U_GROUP_STAFF))
$crtIcon->extraText .= ' [CriteriaId: '.$crt['id'].']';
$extraData = [];
foreach ($crtExtraData[$crt['id']] ?? [] as $xType => $xData)
{
switch ($xType)
{
case ACHIEVEMENT_CRITERIA_DATA_TYPE_T_CREATURE:
$extraData[] = CreatureList::makeLink($xData['value1']);
break;
case ACHIEVEMENT_CRITERIA_DATA_TYPE_T_PLAYER_CLASS_RACE:
case ACHIEVEMENT_CRITERIA_DATA_TYPE_S_PLAYER_CLASS_RACE:
if ($xData['value1'])
$extraData[] = CharClassList::makeLink($xData['value1']);
if ($xData['value2'])
$extraData[] = CharRaceList::makeLink($xData['value2']);
break;
case ACHIEVEMENT_CRITERIA_DATA_TYPE_S_AURA:
case ACHIEVEMENT_CRITERIA_DATA_TYPE_T_AURA:
$extraData[] = SpellList::makeLink($xData['value1']);
break;
case ACHIEVEMENT_CRITERIA_DATA_TYPE_S_AREA:
$extraData[] = ZoneList::makeLink($xData['value1']);
break;
case ACHIEVEMENT_CRITERIA_DATA_TYPE_SCRIPT:
if ($xData['ScriptName'] && User::isInGroup(U_GROUP_STAFF))
$extraData[] = 'Script '.$xData['ScriptName'];
break;
case ACHIEVEMENT_CRITERIA_DATA_TYPE_HOLIDAY:
if ($we = new WorldEventList(array(['holidayId', $xData['value1']])))
$extraData[] = '<a href="?event='.$we->id.'">'.$we->getField('name', true).'</a>';
break;
case ACHIEVEMENT_CRITERIA_DATA_TYPE_MAP_ID:
if ($z = new ZoneList(array(['mapIdBak', $xData['value1']])))
$extraData[] = '<a href="?zone='.$z->id.'">'.$z->getField('name', true).'</a>';
break;
case ACHIEVEMENT_CRITERIA_DATA_TYPE_S_KNOWN_TITLE:
$extraData[] = TitleList::makeLink($xData['value1']);
break;
default:
if (User::isInGroup(U_GROUP_STAFF))
$extraData[] = 'has extra criteria data';
}
}
if ($extraData)
$crtIcon->extraText .= ' <br /><sup style="margin-left:8px;">('.implode(', ', $extraData).')</sup>';
$this->criteria[] = $crtIcon;
}
/**************/
/* Extra Tabs */
/**************/
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true);
// tab: see also
$conditions = array(
['name_loc'.Lang::getLocale()->value, $this->subject->getField('name', true)],
['id', $this->typeId, '!']
);
$saList = new AchievementList($conditions);
if (!$saList->error)
{
$this->extendGlobalData($saList->getJSGlobals());
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $saList->getListviewData(),
'id' => 'see-also',
'name' => '$LANG.tab_seealso',
'visibleCols' => ['category']
), AchievementList::$brickFile));
}
// tab: criteria of
$refs = DB::Aowow()->SelectCol('SELECT `refAchievementId` FROM ?_achievementcriteria WHERE `type` = ?d AND `value1` = ?d',
ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_ACHIEVEMENT,
$this->typeId
);
if (!empty($refs))
{
$coList = new AchievementList(array(['id', $refs]));
if (!$coList->error)
{
$this->extendGlobalData($coList->getJSGlobals());
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $coList->getListviewData(),
'id' => 'criteria-of',
'name' => '$LANG.tab_criteriaof',
'visibleCols' => ['category']
), AchievementList::$brickFile));
}
}
// tab: condition for
$cnd = new Conditions();
$cnd->getByCondition(Type::ACHIEVEMENT, $this->typeId)->prepare();
if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for'))
{
$this->extendGlobalData($cnd->getJsGlobals());
$this->lvTabs->addDataTab(...$tab);
}
parent::generate();
if ($this->subject->getField('flags') & ACHIEVEMENT_FLAG_REALM_FIRST)
$this->result->registerDisplayHook('infobox', [self::class, 'infoboxHook']);
}
private function createMail() : bool
{
if ($_ = $this->subject->getField('mailTemplate'))
{
$letter = DB::Aowow()->selectRow('SELECT * FROM ?_mails WHERE `id` = ?d', $_);
if (!$letter)
return false;
$this->mail = array(
'attachments' => [],
'subject' => Util::parseHtmlText(Util::localizedString($letter, 'subject', true)),
'text' => Util::parseHtmlText(Util::localizedString($letter, 'text', true)),
'header' => [$_, null, null]
);
}
else if ($_ = Util::parseHtmlText($this->subject->getField('text', true, true)))
{
$this->mail = array(
'attachments' => [],
'subject' => Util::parseHtmlText($this->subject->getField('subject', true, true)),
'text' => $_,
'header' => [-$this->typeId, null, null]
);
}
else
return false;
if ($senderId = $this->subject->getField('sender'))
if ($senderName = CreatureList::getName($senderId))
$this->mail['header'][1] = Lang::mail('mailBy', [$senderId, $senderName]);
return true;
}
/* finalize infobox */
public static function infoboxHook(Template\PageTemplate &$pt, ?InfoboxMarkup &$markup) : void
{
// realm first still available?
if (!DB::isConnectable(DB_AUTH))
return;
$avlb = [];
foreach (Profiler::getRealms() AS $rId => $rData)
if (!DB::Characters($rId)->selectCell('SELECT 1 FROM character_achievement WHERE `achievement` = ?d', $pt->typeId))
$avlb[] = Util::ucWords($rData['name']);
if (!$avlb)
return;
$addRow = Lang::achievement('rfAvailable').implode(', ', $avlb);
if (!$markup)
$markup = new InfoboxMarkup([$addRow], ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0');
else
$markup->addItem($addRow);
}
}
?>

View File

@@ -0,0 +1,50 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AchievementPowerResponse extends TextResponse implements ICache
{
use TrCache, TrTooltip;
private const /* string */ POWER_TEMPLATE = '$WowheadPower.registerAchievement(%d, %d, %s);';
protected int $type = Type::ACHIEVEMENT;
protected int $typeId = 0;
protected int $cacheType = CACHE_TYPE_TOOLTIP;
protected array $expectedGET = array(
'domain' => ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']]
);
public function __construct($id)
{
parent::__construct($id);
// temp locale
if ($this->_get['domain'])
Lang::load($this->_get['domain']);
$this->typeId = intVal($id);
}
protected function generate() : void
{
$achievement = new AchievementList(array(['id', $this->typeId]));
if ($achievement->error)
$this->cacheType = CACHE_TYPE_NONE;
else
$opts = array(
'name' => $achievement->getField('name', true),
'tooltip' => $achievement->renderTooltip(),
'icon' => $achievement->getField('iconString')
);
$this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []);
}
}
?>

View File

@@ -0,0 +1,168 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AchievementsBaseResponse extends TemplateResponse implements ICache
{
use TrListPage, TrCache;
protected int $type = Type::ACHIEVEMENT;
protected int $cacheType = CACHE_TYPE_LIST_PAGE;
protected string $template = 'achievements';
protected string $pageName = 'achievements';
protected ?int $activeTab = parent::TAB_DATABASE;
protected array $breadcrumb = [0, 9];
protected array $scripts = [[SC_JS_FILE, 'js/filters.js']];
protected array $expectedGET = array(
'filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]]
);
protected array $validCats = array(
92 => true,
96 => [14861, 14862, 14863],
97 => [14777, 14778, 14779, 14780],
95 => [165, 14801, 14802, 14803, 14804, 14881, 14901, 15003],
168 => [14808, 14805, 14806, 14921, 14922, 14923, 14961, 14962, 15001, 15002, 15041, 15042],
169 => [170, 171, 172],
201 => [14864, 14865, 14866],
155 => [160, 187, 159, 163, 161, 162, 158, 14981, 156, 14941],
81 => true,
1 => array (
130 => [140, 145, 147, 191],
141 => true,
128 => [135, 136, 137],
122 => [123, 124, 125, 126, 127],
133 => true,
14807 => [14821, 14822, 14823, 14963, 15021, 15062],
132 => [178, 173],
134 => true,
131 => true,
21 => [152, 153, 154]
)
);
public function __construct(string $pageParam)
{
$this->getCategoryFromUrl($pageParam);
parent::__construct($pageParam);
$this->subCat = $pageParam !== '' ? '='.$pageParam : '';
$this->filter = new AchievementListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]);
if ($this->filter->shouldReload)
{
$_SESSION['error']['fi'] = $this->filter::class;
$get = $this->filter->buildGETParam();
$this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : ''));
}
$this->filterError = $this->filter->error;
}
protected function generate() : void
{
$this->h1 = Util::ucFirst(Lang::game('achievements'));
$conditions = [];
if (!User::isInGroup(U_GROUP_EMPLOYEE))
$conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0];
// include child categories if current category is empty
if ($this->category)
$conditions[] = ['category', end($this->category)];
if ($fiCnd = $this->filter->getConditions())
$conditions[] = $fiCnd;
/*************/
/* Menu Path */
/*************/
foreach ($this->category as $cat)
$this->breadcrumb[] = $cat;
/**************/
/* Page Title */
/**************/
array_unshift($this->title, Util::ucFirst(Lang::game('achievements')));
if ($this->category)
array_unshift($this->title, Lang::achievement('cat', end($this->category)));
/****************/
/* Main Content */
/****************/
// fix modern client achievement category structure: top catg [1:char, 2:statistic, 3:guild]
if ($this->category && $this->category[0] != 1)
$link = '=1.'.implode('.', $this->category);
else if ($this->category)
$link = '=2'.(count($this->category) > 1 ? '.'.implode('.', array_slice($this->category, 1)) : '');
else
$link = '';
$this->redButtons[BUTTON_WOWHEAD] = true;
$this->wowheadLink = sprintf(WOWHEAD_LINK, Lang::getLocale()->domain(), $this->pageName, $link);
if ($fiQuery = $this->filter->buildGETParam())
$this->wowheadLink .= '&filter='.$fiQuery;
$acvList = new AchievementList($conditions, ['calcTotal' => true]);
if (!$acvList->getMatches() && $this->category)
{
// ToDo - we also branch into here if the filter prohibits results. That should be skipped.
$conditions = [];
if ($fiCnd)
$conditions[] = $fiCnd;
if ($catList = DB::Aowow()->SelectCol('SELECT `id` FROM ?_achievementcategory WHERE `parentCat` IN (?a) OR `parentCat2` IN (?a) ', $this->category, $this->category))
$conditions[] = ['category', $catList];
$acvList = new AchievementList($conditions, ['calcTotal' => true]);
}
$tabData = [];
if (!$acvList->error)
{
$tabData['data'] = $acvList->getListviewData();
// fill g_items, g_titles, g_achievements
$this->extendGlobalData($acvList->getJSGlobals());
// if we are have different cats display field
if ($acvList->hasDiffFields('category'))
$tabData['visibleCols'] = ['category'];
if ($this->filter->fiExtraCols)
$tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)';
// create note if search limit was exceeded
if ($acvList->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT'))
{
$tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_achievementsfound', $acvList->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT'));
$tabData['_truncated'] = 1;
}
}
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]);
$this->lvTabs->addListviewTab(new Listview($tabData, AchievementList::$brickFile));
parent::generate();
$this->setOnCacheLoaded([self::class, 'onBeforeDisplay']);
}
public static function onBeforeDisplay()
{
// sort for dropdown-menus in filter
Lang::sort('game', 'si');
}
}
?>

View File

@@ -0,0 +1,68 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminAnnouncementsResponse extends TemplateResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU;
protected string $template = 'text-page-generic';
protected string $pageName = 'announcements';
protected ?int $activeTab = parent::TAB_STAFF;
protected array $breadcrumb = [4, 1, 3]; // Staff > Content > Announcements
protected array $expectedGET = array(
'id' => ['filter' => FILTER_VALIDATE_INT ],
'edit' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ],
'status' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 2]]
);
protected function generate() : void
{
if ($this->_get['id'] && isset($this->_get['status']))
{
$this->updateStatus();
$this->forward($_SERVER['HTTP_REFERER'] ?? '.');
}
else if ($this->_get['edit'])
$this->displayEditor();
else
$this->displayListing();
parent::generate();
}
private function updateStatus() : void
{
if (!$this->assertGET('status', 'id'))
{
trigger_error('AdminAnnouncementsResponse::updateStatus - error in _GET id/status');
return;
}
if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_announcements WHERE `id` = ?d', $this->_get['id']))
{
trigger_error('AdminAnnouncementsResponse::updateStatus - announcement does not exist');
return;
}
DB::Aowow()->query('UPDATE ?_announcements SET `status` = ?d WHERE `id` = ?d', $this->_get['status'], $this->_get['id']);
}
private function displayEditor() : void
{
// TBD
$this->extraHTML = 'TODO - editor';
}
private function displayListing() : void
{
// TBD
// some form of listview with [NEW] button somewhere near the head i guess
$this->extraHTML = 'TODO - announcements listing';
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminCommentResponse extends TextResponse
{
private const /* int */ ERR_NONE = 1;
private const /* int */ ERR_WRITE_DB = 0;
private const /* int */ ERR_MISCELLANEOUS = 999;
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_MOD;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_VALIDATE_INT ],
'status' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 1]]
);
protected function generate() : void
{
if (!$this->assertPOST('id', 'status'))
{
trigger_error('AdminCommentResponse - malformed request received', E_USER_ERROR);
$this->result = self::ERR_MISCELLANEOUS;
return;
}
// check if is marked as outdated CC_FLAG_OUTDATED?
$ok = false;
if ($this->_post['status']) // outdated, mark as deleted and clear other flags (sticky + outdated)
{
if ($ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = ?d, `deleteUserId` = ?d, `deleteDate` = ?d WHERE `id` = ?d', CC_FLAG_DELETED, User::$id, time(), $this->_post['id']))
if ($rep = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id']))
$rep->close(Report::STATUS_CLOSED_SOLVED);
}
else // up to date
{
if ($ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` = ?d', CC_FLAG_OUTDATED, $this->_post['id']))
if ($rep = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id']))
$rep->close(Report::STATUS_CLOSED_WONTFIX);
}
$this->result = $ok ? self::ERR_NONE : self::ERR_WRITE_DB;
}
}
?>

81
endpoints/admin/guide.php Normal file
View File

@@ -0,0 +1,81 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminGuideResponse extends TextResponse
{
private const /* int */ ERR_NONE = 0;
private const /* int */ ERR_GUIDE = 1;
private const /* int */ ERR_STATUS = 2;
private const /* int */ ERR_WRITE_DB = 3;
private const /* int */ ERR_MISCELLANEOUS = 999;
protected int $requiredUserGroup = U_GROUP_STAFF;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_VALIDATE_INT ],
'status' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => GuideMgr::STATUS_APPROVED, 'max_range' => GuideMgr::STATUS_REJECTED]],
'msg' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ]
);
protected function generate() : void
{
if (!$this->assertPOST('id', 'status'))
{
trigger_error('AdminGuideResponse - malformed request received', E_USER_ERROR);
$this->result = self::ERR_MISCELLANEOUS;
return;
}
$guide = DB::Aowow()->selectRow('SELECT `userId`, `status` FROM ?_guides WHERE `id` = ?d', $this->_post['id']);
if (!$guide)
{
trigger_error('AdminGuideResponse - guide #'.$this->_post['id'].' not found', E_USER_ERROR);
$this->result = self::ERR_GUIDE;
return;
}
if ($this->_post['status'] == $guide['status'])
{
trigger_error('AdminGuideResponse - guide #'.$this->_post['id'].' already has status #'.$this->_post['status'], E_USER_ERROR);
$this->result = self::ERR_STATUS;
return;
}
// status can only be APPROVED or REJECTED due to input validation
if (!$this->update($this->_post['id'], $this->_post['status'], $this->_post['msg']))
{
trigger_error('AdminGuideResponse - write to db failed for guide #'.$this->_post['id'], E_USER_ERROR);
$this->result = self::ERR_WRITE_DB;
return;
}
if ($this->_post['status'] == GuideMgr::STATUS_APPROVED)
Util::gainSiteReputation($guide['userId'], SITEREP_ACTION_ARTICLE, ['id' => $this->_post['id']]);
$this->result = self::ERR_NONE;
}
private function update(int $id, int $status, ?string $msg = null) : bool
{
if ($status == GuideMgr::STATUS_APPROVED) // set display rev to latest
$ok = DB::Aowow()->query('UPDATE ?_guides SET `status` = ?d, `rev` = (SELECT `rev` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d ORDER BY `rev` DESC LIMIT 1), `approveUserId` = ?d, `approveDate` = ?d WHERE `id` = ?d', $status, Type::GUIDE, $id, User::$id, time(), $id);
else
$ok = DB::Aowow()->query('UPDATE ?_guides SET `status` = ?d WHERE `id` = ?d', $status, $id);
if (!$ok)
return false;
DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `status`) VALUES (?d, ?d, ?d, ?d)', $id, time(), User::$id, $status);
if ($msg)
DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `msg`) VALUES (?d, ?d, ?d, ?)', $id, time(), User::$id, $msg);
return true;
}
}
?>

View File

@@ -0,0 +1,46 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminGuidesResponse extends TemplateResponse
{
protected int $requiredUserGroup = U_GROUP_STAFF;
protected string $template = 'list-page-generic';
protected string $pageName = 'guides';
protected ?int $activeTab = parent::TAB_STAFF;
protected array $breadcrumb = [4, 1, 25]; // Staff > Content > Guides Awaiting Approval
protected function generate() : void
{
$this->h1 = 'Pending Guides';
array_unshift($this->title, $this->h1);
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]);
parent::generate();
$pending = new GuideList([['status', GuideMgr::STATUS_REVIEW]]);
if ($pending->error)
$data = [];
else
{
$data = $pending->getListviewData();
$latest = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, MAX(`rev`) FROM ?_articles WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `rev`', Type::GUIDE, $pending->getFoundIDs());
foreach ($latest as $id => $rev)
$data[$id]['rev'] = $rev;
}
$this->lvTabs->addListviewTab(new Listview(array(
'data' => array_values($data),
'hiddenCols' => ['patch', 'comments', 'views', 'rating'],
'extraCols' => '$_'
), GuideList::$brickFile, 'guideAdminCol'));
}
}
?>

View File

@@ -0,0 +1,34 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminOutofdateResponse extends TemplateResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_MOD;
protected string $template = 'list-page-generic';
protected string $pageName = 'out-of-date';
protected ?int $activeTab = parent::TAB_STAFF;
protected array $breadcrumb = [4, 1, 23]; // Staff > Content > Out of Date Comments
protected function generate() : void
{
$this->h1 = 'Out of Date Comments';
array_unshift($this->title, $this->h1);
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]);
parent::generate();
$this->lvTabs->addListviewTab(new Listview(array(
'data' => CommunityContent::getCommentPreviews(['flags' => CC_FLAG_OUTDATED]),
'extraCols' => '$_'
), 'commentpreview', 'commentAdminCol'));
}
}
?>

View File

@@ -0,0 +1,80 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminPhpinfoResponse extends TemplateResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_DEV;
protected string $template = 'list-page-generic';
protected string $pageName = 'phpinfo';
protected ?int $activeTab = parent::TAB_STAFF;
protected array $breadcrumb = [4, 2, 21]; // Staff > Development > PHP Information
protected function generate() : void
{
$this->h1 = 'PHP Information';
array_unshift($this->title, $this->h1);
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]);
parent::generate();
$this->addScript([SC_CSS_STRING, <<<CSS
pre { margin: 0px; font-family: monospace; }
.d, th { border: 1px solid #000000; vertical-align: baseline; }
.p { text-align: left; }
.e { background-color: #ccccff; font-weight: bold; color: #000000; }
.h { background-color: #9999cc; font-weight: bold; color: #000000; }
.v { background-color: #cccccc; color: #000000; }
.vr { background-color: #cccccc; text-align: right; color: #000000; }
CSS]);
$bits = [INFO_GENERAL, INFO_CONFIGURATION, INFO_ENVIRONMENT, INFO_MODULES];
$names = ['General', '', '', 'Module'];
foreach ($bits as $i => $b)
{
ob_start();
phpinfo($b);
$buff = ob_get_contents();
ob_end_clean();
$buff = explode('<div class="center">', $buff)[1];
$buff = explode('</div>', $buff);
array_pop($buff); // remove last from stack
$buff = implode('</div>', $buff); // sew it together
if (strpos($buff, '<h1>'))
$buff = explode('</h1>', $buff)[1];
if (strpos($buff, '<h2>'))
{
$parts = explode('<h2>', $buff);
foreach ($parts as $p)
{
if (!preg_match('/\w/i', $p))
continue;
$p = explode('</h2>', $p);
$name = $names[$i] ? $names[$i].': ' : '';
if (preg_match('/<a[^>]*>([\w\s\d]+)<\/a>/i', $p[0], $m))
$name .= $m[1];
else
$name .= $p[0];
$this->lvTabs->addDataTab(strtolower(strtr($name, [' ' => ''])), $name, $p[1]);
}
}
else
$this->lvTabs->addDataTab(strtolower($names[$i]), $names[$i], $buff);
}
}
}
?>

View File

@@ -0,0 +1,29 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminReportsResponse extends TemplateResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_EDITOR | U_GROUP_MOD | U_GROUP_LOCALIZER | U_GROUP_SCREENSHOT | U_GROUP_VIDEO;
protected string $template = 'admin/reports';
protected string $pageName = 'reports';
protected ?int $activeTab = parent::TAB_STAFF;
protected array $breadcrumb = [4, 5]; // Staff > Reports
protected function generate() : void
{
$this->h1 = 'Reports';
array_unshift($this->title, $this->h1);
$this->extraHTML = 'NYI';
parent::generate();
}
}
?>

View File

@@ -0,0 +1,68 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminScreenshotsResponse extends TemplateResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_SCREENSHOT;
protected string $template = 'admin/screenshots';
protected string $pageName = 'screenshots';
protected ?int $activeTab = parent::TAB_STAFF;
protected array $breadcrumb = [4, 1, 5]; // Staff > Content > Screenshots
protected array $scripts = array(
[SC_JS_FILE, 'js/screenshot.js'],
[SC_CSS_STRING, '.layout {margin: 0px 25px; max-width: inherit; min-width: 1200px; }'],
[SC_CSS_STRING, '#highlightedRow { background-color: #322C1C; }']
);
protected array $expectedGET = array(
'action' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']],
'all' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']],
'type' => ['filter' => FILTER_VALIDATE_INT ],
'typeid' => ['filter' => FILTER_VALIDATE_INT ],
'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode' ]
);
public ?bool $getAll = null;
public array $ssPages = [];
public array $ssData = [];
public int $ssNFound = 0;
public array $pageTypes = [];
protected function generate() : void
{
$this->h1 = 'Screenshot Manager';
// types that can have screenshots
foreach (Type::getClassesFor(0, 'contribute', CONTRIBUTE_SS) as $type => $obj)
$this->pageTypes[$type] = Util::ucWords(Lang::game(Type::getFileString($type)));
$ssGetAll = $this->_get['all'];
$ssPages = [];
$ssData = [];
$nMatches = 0;
if ($this->_get['type'] && $this->_get['typeid'])
$ssData = ScreenshotMgr::getScreenshots($this->_get['type'], $this->_get['typeid'], nFound: $nMatches);
else if ($this->_get['user'])
{
if (mb_strlen($this->_get['user']) >= 3)
if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']))
$ssData = ScreenshotMgr::getScreenshots(userId: $uId, nFound: $nMatches);
}
else
$ssPages = ScreenshotMgr::getPages($ssGetAll, $nMatches);
$this->getAll = $ssGetAll;
$this->ssPages = $ssPages;
$this->ssData = $ssData;
$this->ssNFound = $nMatches; // ssm_numPagesFound
parent::generate();
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Aowow;
use GdImage;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminScreenshotsActionApproveResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_SCREENSHOT;
protected array $expectedGET = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']]
);
protected function generate() : void
{
if (!$this->assertGET('id'))
{
trigger_error('AdminScreenshotsActionApproveResponse - screenshotId empty', E_USER_ERROR);
return;
}
ScreenshotMgr::init();
// create resized and thumb version of screenshot
$ssEntries = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId` FROM ?_screenshots WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_APPROVED, $this->_get['id']);
foreach ($ssEntries as $id => $ssData)
{
if (!ScreenshotMgr::loadFile(ScreenshotMgr::PATH_PENDING, $id))
continue;
if (!ScreenshotMgr::createResized($id))
continue;
if (!ScreenshotMgr::createThumbnail($id))
continue;
// move pending > normal
if (!rename(sprintf(ScreenshotMgr::PATH_PENDING, $id), sprintf(ScreenshotMgr::PATH_NORMAL, $id)))
continue;
// set as approved in DB
DB::Aowow()->query('UPDATE ?_screenshots SET `status` = ?d, `userIdApprove` = ?d WHERE `id` = ?d', CC_FLAG_APPROVED, User::$id, $id);
// gain siterep
Util::gainSiteReputation($ssData['userIdOwner'], SITEREP_ACTION_SUBMIT_SCREENSHOT, ['id' => $id, 'what' => 1, 'date' => $ssData['date']]);
// flag DB entry as having screenshots
if ($tbl = Type::getClassAttrib($ssData['type'], 'dataTable'))
DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_SCREENSHOT, $ssData['typeId']);
unset($ssEntries[$id]);
}
if (!$ssEntries)
trigger_error('AdminScreenshotsActionApproveResponse - screenshot(s) # '.implode(', ', array_keys($ssEntries)).' not in db or already approved', E_USER_WARNING);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminScreenshotsActionDeleteResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_SCREENSHOT;
protected array $expectedGET = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']]
);
// 2 steps: 1) remove from sight, 2) remove from disk
protected function generate() : void
{
if (!$this->assertGET('id'))
{
trigger_error('AdminScreenshotsActionDeleteResponse - screenshotId empty', E_USER_ERROR);
return;
}
foreach ($this->_get['id'] as $id)
{
// irrevocably purge files already flagged as deleted (should only exist as pending)
if (User::isInGroup(U_GROUP_ADMIN) && DB::Aowow()->selectCell('SELECT 1 FROM ?_screenshots WHERE `status` & ?d AND `id` = ?d', CC_FLAG_DELETED, $id))
{
DB::Aowow()->query('DELETE FROM ?_screenshots WHERE `id` = ?d', $id);
if (file_exists(sprintf(ScreenshotMgr::PATH_PENDING, $id)))
unlink(sprintf(ScreenshotMgr::PATH_PENDING, $id));
continue;
}
// move normal to pending and remove resized and thumb
if (file_exists(sprintf(ScreenshotMgr::PATH_NORMAL, $id)))
rename(sprintf(ScreenshotMgr::PATH_NORMAL, $id), sprintf(ScreenshotMgr::PATH_PENDING, $id));
if (file_exists(sprintf(ScreenshotMgr::PATH_THUMB, $id)))
unlink(sprintf(ScreenshotMgr::PATH_THUMB, $id));
if (file_exists(sprintf(ScreenshotMgr::PATH_RESIZED, $id)))
unlink(sprintf(ScreenshotMgr::PATH_RESIZED, $id));
}
// flag as deleted if not aready
$oldEntries = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, GROUP_CONCAT(`typeId`) FROM ?_screenshots WHERE `id` IN (?a) GROUP BY `type`', $this->_get['id']);
DB::Aowow()->query('UPDATE ?_screenshots SET `status` = ?d, `userIdDelete` = ?d WHERE `id` IN (?a)', CC_FLAG_DELETED, User::$id, $this->_get['id']);
// deflag db entry as having screenshots
foreach ($oldEntries as $type => $typeIds)
{
$typeIds = explode(',', $typeIds);
$toUnflag = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(BIT_OR(`status`) & ?d, 1, 0) AS "hasMore" FROM ?_screenshots WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `typeId` HAVING `hasMore` = 0', CC_FLAG_APPROVED, $type, $typeIds);
if ($toUnflag && ($tbl = Type::getClassAttrib($type, 'dataTable')))
DB::Aowow()->query('UPDATE ?# SET cuFlags = cuFlags & ~?d WHERE id IN (?a)', $tbl, CUSTOM_HAS_SCREENSHOT, array_keys($toUnflag));
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminScreenshotsActionEditaltResponse extends TextResponse
{
use TrCommunityHelper;
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_SCREENSHOT;
protected array $expectedGET = array(
'id' => ['filter' => FILTER_VALIDATE_INT]
);
protected array $expectedPOST = array(
'alt' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']]
);
protected function generate() : void
{
if (!$this->assertGET('id'))
return;
DB::Aowow()->query('UPDATE ?_screenshots SET `caption` = ? WHERE `id` = ?d',
$this->handleCaption($this->_post['alt']),
$this->_get['id']
);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminScreenshotsActionListResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_SCREENSHOT;
protected array $expectedGET = array(
'all' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']]
);
protected function generate() : void
{
$pages = ScreenshotMgr::getPages($this->_get['all'], $nPages);
$this->result = 'ssm_screenshotPages = '.Util::toJSON($pages).";\n";
$this->result .= 'ssm_numPagesFound = '.$nPages.';';
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminScreenshotsActionManageResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_SCREENSHOT;
protected array $expectedGET = array(
'type' => ['filter' => FILTER_VALIDATE_INT ],
'typeid' => ['filter' => FILTER_VALIDATE_INT ],
'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode']
);
protected function generate() : void
{
$res = [];
if ($this->_get['type'] && $this->_get['typeid'])
$res = ScreenshotMgr::getScreenshots($this->_get['type'], $this->_get['typeid']);
else if ($this->_get['user'])
if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']))
$res = ScreenshotMgr::getScreenshots(userId: $uId);
$this->result = 'ssm_screenshotData = '.Util::toJSON($res);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminScreenshotsActionRelocateResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_SCREENSHOT;
protected array $expectedGET = array(
'id' => ['filter' => FILTER_VALIDATE_INT],
'typeid' => ['filter' => FILTER_VALIDATE_INT]
// (but not type..?)
);
protected function generate() : void
{
if (!$this->assertGET('id', 'typeid'))
{
trigger_error('AdminScreenshotsActionRelocateResponse - screenshotId or typeId empty', E_USER_ERROR);
return;
}
[$type, $oldTypeId] = array_values(DB::Aowow()->selectRow('SELECT `type`, `typeId` FROM ?_screenshots WHERE `id` = ?d', $this->_get['id']));
$typeId = $this->_get['typeid'];
if (Type::validateIds($type, $typeId))
{
$tbl = Type::getClassAttrib($type, 'dataTable');
// move screenshot
DB::Aowow()->query('UPDATE ?_screenshots SET `typeId` = ?d WHERE `id` = ?d', $typeId, $this->_get['id']);
// flag target as having screenshot
DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_SCREENSHOT, $typeId);
// deflag source for having had screenshots (maybe)
$ssInfo = DB::Aowow()->selectRow('SELECT IF(BIT_OR(~`status`) & ?d, 1, 0) AS "hasMore" FROM ?_screenshots WHERE `status`& ?d AND `type` = ?d AND `typeId` = ?d', CC_FLAG_DELETED, CC_FLAG_APPROVED, $type, $oldTypeId);
if ($ssInfo || !$ssInfo['hasMore'])
DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` & ~?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_SCREENSHOT, $oldTypeId);
}
else
trigger_error('AdminScreenshotsActionRelocateResponse - invalid typeId #'.$typeId.' for type #'.$type, E_USER_ERROR);
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminScreenshotsActionStickyResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_SCREENSHOT;
protected array $expectedGET = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']]
);
protected function generate() : void
{
if (!$this->assertGET('id'))
{
trigger_error('AdminScreenshotsActionStickyResponse - screenshotId empty', E_USER_ERROR);
return;
}
// this one is a bit strange: as far as i've seen, the only thing a 'sticky' screenshot does is show up in the infobox
// this also means, that only one screenshot per page should be sticky
// so, handle it one by one and the last one affecting one particular type/typId-key gets the cake
$ssEntries = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId`, `status` FROM ?_screenshots WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_DELETED, $this->_get['id']);
foreach ($ssEntries as $id => $ssData)
{
// approve yet unapproved screenshots
if (!($ssData['status'] & CC_FLAG_APPROVED))
{
ScreenshotMgr::init();
if (!ScreenshotMgr::loadFile(ScreenshotMgr::PATH_PENDING, $id))
continue;
if (!ScreenshotMgr::createResized($id))
continue;
if (!ScreenshotMgr::createThumbnail($id))
continue;
// move pending > normal
if (!rename(sprintf(ScreenshotMgr::PATH_PENDING, $id), sprintf(ScreenshotMgr::PATH_NORMAL, $id)))
continue;
// set as approved in DB
DB::Aowow()->query('UPDATE ?_screenshots SET `status` = ?d, `userIdApprove` = ?d WHERE `id` = ?d', CC_FLAG_APPROVED, User::$id, $id);
// gain siterep
Util::gainSiteReputation($ssData['userIdOwner'], SITEREP_ACTION_SUBMIT_SCREENSHOT, ['id' => $id, 'what' => 1, 'date' => $ssData['date']]);
// flag DB entry as having screenshots
if ($tbl = Type::getClassAttrib($ssData['type'], 'dataTable'))
DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_SCREENSHOT, $ssData['typeId']);
}
// reset all others
DB::Aowow()->query('UPDATE ?_screenshots a, ?_screenshots b SET a.`status` = a.`status` & ~?d WHERE a.`type` = b.`type` AND a.`typeId` = b.`typeId` AND a.`id` <> b.`id` AND b.`id` = ?d', CC_FLAG_STICKY, $id);
// toggle sticky status
DB::Aowow()->query('UPDATE ?_screenshots SET `status` = IF(`status` & ?d, `status` & ~?d, `status` | ?d) WHERE `id` = ?d AND `status` & ?d', CC_FLAG_STICKY, CC_FLAG_STICKY, CC_FLAG_STICKY, $id, CC_FLAG_APPROVED);
unset($ssEntries[$id]);
}
if ($ssEntries)
trigger_error('AdminScreenshotsActionStickyResponse - screenshot(s) # '.implode(', ', array_keys($ssEntries)).' not in db or flagged as deleted', E_USER_WARNING);
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminSiteconfigResponse extends TemplateResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_DEV;
protected string $template = 'admin/siteconfig';
protected string $pageName = 'siteconfig';
protected ?int $activeTab = parent::TAB_STAFF;
protected array $breadcrumb = [4, 2, 18]; // Staff > Development > Site Configuration
protected function generate() : void
{
$this->h1 = 'Site Configuration';
array_unshift($this->title, $this->h1);
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]);
parent::generate();
$this->addScript([SC_CSS_STRING, <<<CSS
.grid input[type='text'], .grid input[type='number'] { width:250px; text-align:left; }
.grid input[type='button'] { width:65px; padding:2px; }
.grid a.tip { margin:0px 5px; opacity:0.8; }
.grid a.tip:hover { opacity:1; }
.grid tr { height:30px; }
.grid .disabled { opacity:0.4 !important; }
.grid .status { position:absolute; right:5px; }
CSS]);
$head = '<tr><th><b>Key</b></th><th><b>Value</b></th><th style="width:150px;"><b>Options</b></th></tr>';
foreach (Cfg::$categories as $idx => $catName)
{
$rows = '';
foreach (Cfg::forCategory($idx) as $key => [$value, $flags, , $default, $comment])
$rows .= $this->buildRow($key, $value, $flags, $default, $comment);
if ($idx == Cfg::CAT_MISCELLANEOUS)
$rows .= '<tr><td colspan="3"><a class="icon-add" onclick="cfg_add(this)">new configuration</a></td></tr>';
if (!$rows)
continue;
$this->lvTabs->addDataTab(Profiler::urlize($catName), $catName, '<table class="grid">' . $head . $rows . '</table>');
}
}
private function buildRow(string $key, string $value, int $flags, ?string $default, string $comment) : string
{
$buff = '<tr>';
$info = explode(' - ', $comment);
$key = $flags & Cfg::FLAG_PHP ? strtolower($key) : strtoupper($key);
// name
if (!empty($info[0]))
$buff .= '<td>'.sprintf(Util::$dfnString, $info[0], $key).'</td>';
else
$buff .= '<td>'.$key.'</td>';
// value
if ($flags & Cfg::FLAG_TYPE_BOOL)
$buff .= '<td><div id="'.$key.'"><input id="'.$key.'1" type="radio" name="'.$key.'" value="1" '.($value ? 'checked' : null).' /><label for="'.$key.'1">Enabled</label> <input id="'.$key.'0" type="radio" name="'.$key.'" value="0" '.($value ? null : 'checked').' /><label for="'.$key.'0">Disabled</label></div></td>';
else if ($flags & Cfg::FLAG_OPT_LIST && !empty($info[1]))
{
$buff .= '<td><select id="'.$key.'" name="'.$key.'">';
foreach (explode(', ', $info[1]) as $option)
{
[$idx, $name] = explode(':', $option);
$buff .= '<option value="'.$idx.'"'.($value == $idx ? ' selected ' : null).'>'.$name.'</option>';
}
$buff .= '</select></td>';
}
else if ($flags & Cfg::FLAG_BITMASK && !empty($info[1]))
{
$buff .= '<td><div id="'.$key.'">';
foreach (explode(', ', $info[1]) as $option)
{
[$idx, $name] = explode(':', $option);
$buff .= '<input id="'.$key.$idx.'" type="checkbox" name="'.$key.'" value="'.$idx.'"'.($value & (1 << $idx) ? ' checked ' : null).'><label for="'.$key.$idx.'">'.$name.'</label>';
}
$buff .= '</div></td>';
}
else
$buff .= '<td><input id="'.$key.'" type="'.($flags & Cfg::FLAG_TYPE_STRING ? 'text" placeholder="<empty>' : 'number'.($flags & Cfg::FLAG_TYPE_FLOAT ? '" step="any' : '')).'" name="'.$key.'" value="'.$value.'" /></td>';
// actions
$buff .= '<td style="position:relative;">';
$buff .= '<a class="icon-save tip" onclick="cfg_submit.bind(this, \''.$key.'\')()" onmouseover="$WH.Tooltip.showAtCursor(event, \'Save Changes\', 0, 0, \'q\')" onmousemove="$WH.Tooltip.cursorUpdate(event)" onmouseout="$WH.Tooltip.hide()"></a>';
if ($default)
$buff .= '|<a class="icon-refresh tip" onclick="cfg_default(\''.$key.'\', \''.$default.'\')" onmouseover="$WH.Tooltip.showAtCursor(event, \'Restore Default Value\', 0, 0, \'q\')" onmousemove="$WH.Tooltip.cursorUpdate(event)" onmouseout="$WH.Tooltip.hide()"></a>';
else
$buff .= '|<a class="icon-refresh tip disabled"></a>';
if (!($flags & Cfg::FLAG_PERSISTENT))
$buff .= '|<a class="icon-delete tip" onclick="cfg_remove.bind(this, \''.$key.'\')()" onmouseover="$WH.Tooltip.showAtCursor(event, \'Remove Setting\', 0, 0, \'q\')" onmousemove="$WH.Tooltip.cursorUpdate(event)" onmouseout="$WH.Tooltip.hide()"></a>';
$buff .= '<span class="status"></span></td></tr>';
return $buff;
}
}
?>

View File

@@ -0,0 +1,34 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminSiteconfigActionAddResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_DEV | U_GROUP_ADMIN;
protected array $expectedGET = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]],
'val' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ]
);
protected function generate() : void
{
if (!$this->assertGET('key', 'val'))
{
trigger_error('AdminSiteconfigActionAddResponse - malformed request received', E_USER_ERROR);
$this->result = Lang::main('intError');
return;
}
$key = trim($this->_get['key']);
$val = trim(urldecode($this->_get['val']));
$this->result = Cfg::add($key, $val);
}
}
?>

View File

@@ -0,0 +1,30 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminSiteconfigActionRemoveResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_DEV | U_GROUP_ADMIN;
protected array $expectedGET = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]]
);
protected function generate() : void
{
if (!$this->assertGET('key'))
{
trigger_error('AdminSiteconfigActionRemoveResponse - malformed request received', E_USER_ERROR);
$this->result = Lang::main('intError');
return;
}
$this->result = Cfg::delete($this->_get['key']);
}
}
?>

View File

@@ -0,0 +1,34 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminSiteconfigActionUpdateResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_DEV | U_GROUP_ADMIN;
protected array $expectedGET = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]],
'val' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ]
);
protected function generate() : void
{
if (!$this->assertGET('key', 'val'))
{
trigger_error('AdminSiteconfigActionUpdateResponse - malformed request received', E_USER_ERROR);
$this->result = Lang::main('intError');
return;
}
$key = trim($this->_get['key']);
$val = trim(urldecode($this->_get['val']));
$this->result = Cfg::set($key, $val);
}
}
?>

View File

@@ -0,0 +1,116 @@
<?php
namespace Aowow;
use Error;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminSpawnoverrideResponse extends TextResponse
{
private const /* int */ ERR_NONE = 0;
private const /* int */ ERR_NO_POINTS = 1;
private const /* int */ ERR_WORLD_POS = 2;
private const /* int */ ERR_WRONG_TYPE = 3;
private const /* int */ ERR_WRITE_DB = 4;
private const /* int */ ERR_MISCELLANEOUS = 999;
protected int $requiredUserGroup = U_GROUP_MODERATOR;
protected array $expectedGET = array(
'type' => ['filter' => FILTER_VALIDATE_INT],
'guid' => ['filter' => FILTER_VALIDATE_INT],
'area' => ['filter' => FILTER_VALIDATE_INT],
'floor' => ['filter' => FILTER_VALIDATE_INT]
);
protected function generate() : void
{
if (!$this->assertGET('type', 'guid', 'area', 'floor'))
{
trigger_error('AdminSpawnoverrideResponse - malformed request received', E_USER_ERROR);
$this->result = self::ERR_MISCELLANEOUS;
return;
}
$guid = $this->_get['guid'];
$type = $this->_get['type'];
$area = $this->_get['area'];
$floor = $this->_get['floor'];
if (!in_array($type, [Type::NPC, Type::OBJECT, Type::SOUND, Type::AREATRIGGER, Type::ZONE]))
{
trigger_error('AdminSpawnoverrideResponse - can\'t move pip of type '.Type::getFileString($type), E_USER_ERROR);
$this->result = self::ERR_WRONG_TYPE;
return;
}
DB::Aowow()->query('REPLACE INTO ?_spawns_override (`type`, `typeGuid`, `areaId`, `floor`, `revision`) VALUES (?d, ?d, ?d, ?d, ?d)', $type, $guid, $area, $floor, AOWOW_REVISION);
$wPos = WorldPosition::getForGUID($type, $guid);
if (!$wPos)
{
$this->result = self::ERR_WORLD_POS;
return;
}
$point = WorldPosition::toZonePos($wPos[$guid]['mapId'], $wPos[$guid]['posX'], $wPos[$guid]['posY'], $area, $floor);
if (!$point)
{
$this->result = self::ERR_NO_POINTS;
return;
}
$updGUIDs = [$guid];
$newPos = array(
'posX' => $point[0]['posX'],
'posY' => $point[0]['posY'],
'areaId' => $point[0]['areaId'],
'floor' => $point[0]['floor']
);
// if creature try for waypoints
if ($type == Type::NPC)
{
$jobs = array(
'SELECT -w.`id` AS "entry", w.`point` AS "pointId", w.`position_x` AS "posX", w.`position_y` AS "posY" FROM creature_addon ca JOIN waypoint_data w ON w.`id` = ca.`path_id` WHERE ca.`guid` = ?d AND ca.`path_id` <> 0',
'SELECT `entry`, `pointId`, `location_x` AS "posX", `location_y` AS "posY" FROM `script_waypoint` WHERE `entry` = ?d',
'SELECT `entry`, `pointId`, `position_x` AS "posX", `position_y` AS "posY" FROM `waypoints` WHERE `entry` = ?d'
);
foreach ($jobs as $idx => $job)
{
if ($swp = DB::World()->select($job, $idx ? $wPos[$guid]['id'] : $guid))
{
foreach ($swp as $w)
{
if ($point = WorldPosition::toZonePos($wPos[$guid]['mapId'], $w['posX'], $w['posY'], $area, $floor))
{
$p = array(
'posX' => $point[0]['posX'],
'posY' => $point[0]['posY'],
'areaId' => $point[0]['areaId'],
'floor' => $point[0]['floor']
);
DB::Aowow()->query('UPDATE ?_creature_waypoints SET ?a WHERE `creatureOrPath` = ?d AND `point` = ?d', $p, $w['entry'], $w['pointId']);
}
}
}
}
// also move linked vehicle accessories (on the very same position)
$updGUIDs = array_merge($updGUIDs, DB::Aowow()->selectCol('SELECT s2.`guid` FROM ?_spawns s1 JOIN ?_spawns s2 ON s1.`posX` = s2.`posX` AND s1.`posY` = s2.`posY` AND
s1.`areaId` = s2.`areaId` AND s1.`floor` = s2.`floor` AND s2.`guid` < 0 WHERE s1.`guid` = ?d', $guid));
}
if (DB::Aowow()->query('UPDATE ?_spawns SET ?a WHERE `type` = ?d AND `guid` IN (?a)', $newPos, $type, $updGUIDs))
$this->result = self::ERR_NONE;
else
$this->result = self::ERR_WRITE_DB;
}
}
?>

View File

@@ -0,0 +1,68 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminVideosResponse extends TemplateResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO;
protected string $template = 'admin/videos';
protected string $pageName = 'videos';
protected ?int $activeTab = parent::TAB_STAFF;
protected array $breadcrumb = [4, 1, 17]; // Staff > Content > Videos
protected array $scripts = array(
[SC_JS_FILE, 'js/video.js'],
[SC_CSS_STRING, '.layout {margin: 0px 25px; max-width: inherit; min-width: 1200px; }'],
[SC_CSS_STRING, '#highlightedRow { background-color: #322C1C; }']
);
protected array $expectedGET = array(
'action' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']],
'all' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']],
'type' => ['filter' => FILTER_VALIDATE_INT ],
'typeid' => ['filter' => FILTER_VALIDATE_INT ],
'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode' ]
);
public ?bool $getAll = null;
public array $viPages = [];
public array $viData = [];
public int $viNFound = 0;
public array $pageTypes = [];
protected function generate() : void
{
$this->h1 = 'Video Manager';
// types that can have videos
foreach (Type::getClassesFor(0, 'contribute', CONTRIBUTE_SS) as $type => $obj)
$this->pageTypes[$type] = Util::ucWords(Lang::game(Type::getFileString($type)));
$viGetAll = $this->_get['all'];
$viPages = [];
$viData = [];
$nMatches = 0;
if ($this->_get['type'] && $this->_get['typeid'])
$viData = VideoMgr::getVideos($this->_get['type'], $this->_get['typeid'], nFound: $nMatches);
else if ($this->_get['user'])
{
if (mb_strlen($this->_get['user']) >= 3)
if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']))
$viData = VideoMgr::getVideos(userId: $uId, nFound: $nMatches);
}
else
$viPages = VideoMgr::getPages($viGetAll, $nMatches);
$this->getAll = $viGetAll;
$this->viPages = $viPages;
$this->viData = $viData;
$this->viNFound = $nMatches; // ssm_numPagesFound
parent::generate();
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Aowow;
use GdImage;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminVideosActionApproveResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO;
protected array $expectedGET = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']]
);
protected function generate() : void
{
if (!$this->assertGET('id'))
{
trigger_error('AdminVideosActionApproveResponse - videoId empty', E_USER_ERROR);
return;
}
$viEntries = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId` FROM ?_videos WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_APPROVED, $this->_get['id']);
foreach ($viEntries as $id => $viData)
{
// set as approved in DB
DB::Aowow()->query('UPDATE ?_videos SET `status` = ?d, `userIdApprove` = ?d WHERE `id` = ?d', CC_FLAG_APPROVED, User::$id, $id);
// gain siterep
Util::gainSiteReputation($viData['userIdOwner'], SITEREP_ACTION_SUGGEST_VIDEO, ['id' => $id, 'what' => 1, 'date' => $viData['date']]);
// flag DB entry as having videos
if ($tbl = Type::getClassAttrib($viData['type'], 'dataTable'))
DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $viData['typeId']);
unset($viEntries[$id]);
}
if (!$viEntries)
trigger_error('AdminVideosActionApproveResponse - video(s) # '.implode(', ', array_keys($viEntries)).' not in db or already approved', E_USER_WARNING);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminVideosActionDeleteResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO;
protected array $expectedGET = array(
'all' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']]
);
// 2 steps: 1) remove from sight, 2) remove from disk
protected function generate() : void
{
if (!$this->assertGET('id'))
{
trigger_error('AdminVideosActionDeleteResponse - videoId empty', E_USER_ERROR);
return;
}
// irrevocably purge files already flagged as deleted (should only exist as pending)
if (User::isInGroup(U_GROUP_ADMIN))
DB::Aowow()->selectCell('SELECT 1 FROM ?_videos WHERE `status` & ?d AND `id` IN (?a)', CC_FLAG_DELETED, $this->_get['id']);
// flag as deleted if not aready
$oldEntries = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, GROUP_CONCAT(`typeId`) FROM ?_videos WHERE `id` IN (?a) GROUP BY `type`', $this->_get['id']);
DB::Aowow()->query('UPDATE ?_videos SET `status` = ?d, `userIdDelete` = ?d WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_DELETED, User::$id, CC_FLAG_DELETED, $this->_get['id']);
// deflag db entry as having videos
foreach ($oldEntries as $type => $typeIds)
{
$typeIds = explode(',', $typeIds);
$toUnflag = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(BIT_OR(`status`) & ?d, 1, 0) AS "hasMore" FROM ?_videos WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `typeId` HAVING `hasMore` = 0', CC_FLAG_APPROVED, $type, $typeIds);
if ($toUnflag && ($tbl = Type::getClassAttrib($type, 'dataTable')))
DB::Aowow()->query('UPDATE ?# SET cuFlags = cuFlags & ~?d WHERE id IN (?a)', $tbl, CUSTOM_HAS_VIDEO, array_keys($toUnflag));
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminVideosActionEdittitleResponse extends TextResponse
{
use TrCommunityHelper;
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO;
protected array $expectedGET = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']]
);
protected array $expectedPOST = array(
'title' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']]
);
protected function generate() : void
{
if (!$this->assertGET('id'))
return;
$caption = $this->handleCaption($this->_post['title']);
DB::Aowow()->query('UPDATE ?_videos SET `caption` = ? WHERE `id` = ?d', $caption, $this->_get['id'][0]);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminVideosActionListResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO;
protected array $expectedGET = array(
'all' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']]
);
protected function generate() : void
{
$pages = VideoMgr::getPages($this->_get['all'], $nPages);
$this->result = 'vim_videoPages = '.Util::toJSON($pages).";\n";
$this->result .= 'vim_numPagesFound = '.$nPages.';';
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminVideosActionManageResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO;
protected array $expectedGET = array(
'type' => ['filter' => FILTER_VALIDATE_INT ],
'typeid' => ['filter' => FILTER_VALIDATE_INT ],
'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode']
);
protected function generate() : void
{
$res = [];
if ($this->_get['type'] && $this->_get['typeid'])
$res = VideoMgr::getVideos($this->_get['type'], $this->_get['typeid']);
else if ($this->_get['user'])
if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']))
$res = VideoMgr::getVideos(userId: $uId);
$this->result = 'vim_videoData = '.Util::toJSON($res);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminVideosActionOrderResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO;
protected array $expectedGET = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned'] ],
'move' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => -1, 'max_range' => 1]] // -1 = up, 1 = down
);
protected function generate() : void
{
if (!$this->assertGET('id', 'move') || $this->_get['move'] === 0)
{
trigger_error('AdminVideosActionOrderResponse - id or move empty', E_USER_ERROR);
return;
}
$id = $this->_get['id'][0];
$videos = DB::Aowow()->selectCol('SELECT a.`id` AS ARRAY_KEY, a.`pos` FROM ?_videos a, ?_videos b WHERE a.`type` = b.`type` AND a.`typeId` = b.`typeId` AND (a.`status` & ?d) = 0 AND b.`id` = ?d ORDER BY a.`pos` ASC', CC_FLAG_DELETED, $id);
if (!$videos || count($videos) == 1)
{
trigger_error('AdminVideosActionOrderResponse - not enough videos to sort', E_USER_WARNING);
return;
}
$dir = $this->_get['move'];
$curPos = $videos[$id];
if ($dir == -1 && $curPos == 0)
{
trigger_error('AdminVideosActionOrderResponse - video #'.$id.' already in top position', E_USER_WARNING);
return;
}
if ($dir == 1 && $curPos + 1 == count($videos))
{
trigger_error('AdminVideosActionOrderResponse - video #'.$id.' already in bottom position', E_USER_WARNING);
return;
}
$oldKey = array_search($curPos + $dir, $videos);
$videos[$oldKey] -= $dir;
$videos[$id] += $dir;
foreach ($videos as $id => $pos)
DB::Aowow()->query('UPDATE ?_videos SET `pos` = ?d WHERE `id` = ?d', $pos, $id);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminVideosActionRelocateResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO;
protected array $expectedGET = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']],
'typeid' => ['filter' => FILTER_VALIDATE_INT ]
// (but not type..?)
);
protected function generate() : void
{
if (!$this->assertGET('id', 'typeid'))
{
trigger_error('AdminVideosActionRelocateResponse - videoId or typeId empty', E_USER_ERROR);
return;
}
$id = $this->_get['id'][0];
[$type, $oldTypeId] = array_values(DB::Aowow()->selectRow('SELECT `type`, `typeId` FROM ?_videos WHERE `id` = ?d', $id));
$typeId = $this->_get['typeid'];
if (Type::validateIds($type, $typeId))
{
$tbl = Type::getClassAttrib($type, 'dataTable');
// move video
DB::Aowow()->query('UPDATE ?_videos SET `typeId` = ?d WHERE `id` = ?d', $typeId, $id);
// flag target as having video
DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $typeId);
// deflag source for having had videos (maybe)
$viInfo = DB::Aowow()->selectRow('SELECT IF(BIT_OR(~`status`) & ?d, 1, 0) AS "hasMore" FROM ?_videos WHERE `status`& ?d AND `type` = ?d AND `typeId` = ?d', CC_FLAG_DELETED, CC_FLAG_APPROVED, $type, $oldTypeId);
if ($viInfo || !$viInfo['hasMore'])
DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` & ~?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $oldTypeId);
}
else
trigger_error('AdminVideosActionRelocateResponse - invalid typeId #'.$typeId.' for type #'.$type, E_USER_ERROR);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminVideosActionStickyResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO;
protected array $expectedGET = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']]
);
protected function generate() : void
{
if (!$this->assertGET('id'))
{
trigger_error('AdminVideosActionStickyResponse - videoId empty', E_USER_ERROR);
return;
}
// this one is a bit strange: as far as i've seen, the only thing a 'sticky' video does is show up in the infobox
// this also means, that only one video per page should be sticky
// so, handle it one by one and the last one affecting one particular type/typId-key gets the cake
$viEntries = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId`, `status` FROM ?_videos WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_DELETED, $this->_get['id']);
foreach ($viEntries as $id => $viData)
{
// approve yet unapproved videos
if (!($viData['status'] & CC_FLAG_APPROVED))
{
// set as approved in DB
DB::Aowow()->query('UPDATE ?_videos SET `status` = ?d, `userIdApprove` = ?d WHERE `id` = ?d', CC_FLAG_APPROVED, User::$id, $id);
// gain siterep
Util::gainSiteReputation($viData['userIdOwner'], SITEREP_ACTION_SUGGEST_VIDEO, ['id' => $id, 'what' => 1, 'date' => $viData['date']]);
// flag DB entry as having videos
if ($tbl = Type::getClassAttrib($viData['type'], 'dataTable'))
DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $viData['typeId']);
}
// reset all others
DB::Aowow()->query('UPDATE ?_videos a, ?_videos b SET a.`status` = a.`status` & ~?d WHERE a.`type` = b.`type` AND a.`typeId` = b.`typeId` AND a.`id` <> b.`id` AND b.`id` = ?d', CC_FLAG_STICKY, $id);
// toggle sticky status
DB::Aowow()->query('UPDATE ?_videos SET `status` = IF(`status` & ?d, `status` & ~?d, `status` | ?d) WHERE `id` = ?d AND `status` & ?d', CC_FLAG_STICKY, CC_FLAG_STICKY, CC_FLAG_STICKY, $id, CC_FLAG_APPROVED);
unset($viEntries[$id]);
}
if ($viEntries)
trigger_error('AdminVideosActionStickyResponse - video(s) # '.implode(', ', array_keys($viEntries)).' not in db or flagged as deleted', E_USER_WARNING);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminWeightpresetsResponse extends TemplateResponse
{
protected int $requiredUserGroup = U_GROUP_ADMIN | U_GROUP_DEV | U_GROUP_BUREAU;
protected string $template = 'admin/weight-presets';
protected string $pageName = 'weight-presets';
protected ?int $activeTab = parent::TAB_STAFF;
protected array $breadcrumb = [4, 2, 16]; // Staff > Development > Weight Presets
protected array $scripts = array(
[SC_JS_FILE, 'js/filters.js'],
[SC_CSS_STRING, '.wt-edit {display:inline-block; vertical-align:top; width:350px;}']
);
protected function generate() : void
{
$this->h1 = 'Weight Presets';
array_unshift($this->title, $this->h1);
$head = $body = '';
$scales = DB::Aowow()->select('SELECT `class` AS ARRAY_KEY, `id` AS ARRAY_KEY2, `name`, `icon` FROM ?_account_weightscales WHERE `userId` = 0');
$weights = DB::Aowow()->selectCol('SELECT awd.`id` AS ARRAY_KEY, awd.`field` AS ARRAY_KEY2, awd.`val` FROM ?_account_weightscale_data awd JOIN ?_account_weightscales ad ON awd.`id` = ad.`id` WHERE ad.`userId` = 0');
foreach ($scales as $cl => $data)
{
$ul = '';
foreach ($data as $id => $s)
{
$weights[$id]['__icon'] = $s['icon'];
$ul .= '[url=# onclick="loadScale.bind(this, '.$id.')();"]'.$s['name'].'[/url][br]';
}
$head .= '[td=header][class='.$cl.'][/td]';
$body .= '[td valign=top]'.$ul.'[/td]';
$this->extendGlobalIds(Type::CHR_CLASS, $cl);
}
$this->extraText = new Markup('[table class=grid][tr]'.$head.'[/tr][tr]'.$body.'[/tr][/table]', ['allow' => Markup::CLASS_ADMIN], 'text-generic');
$this->extraHTML = '<script type="text/javascript">var wt_presets = '.Util::toJSON($weights).";</script>\n\n";
parent::generate();
}
}
?>

View File

@@ -0,0 +1,74 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AdminWeightpresetsActionSaveResponse extends TextResponse
{
private const /* int */ ERR_NONE = 0;
private const /* int */ ERR_WRITE_DB = 1;
private const /* int */ ERR_WRITE_FILE = 2;
private const /* int */ ERR_MISCELLANEOUS = 999;
protected int $requiredUserGroup = U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_BUREAU;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_VALIDATE_INT ],
'__icon' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]],
'scale' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkScale'] ]
);
protected function generate() : void
{
if (!$this->assertPOST('id', '__icon', 'scale'))
{
trigger_error('AdminWeightpresetsActionSaveResponse - malformed request received', E_USER_ERROR);
$this->result = self::ERR_MISCELLANEOUS;
return;
}
// save to db
DB::Aowow()->query('DELETE FROM ?_account_weightscale_data WHERE `id` = ?d', $this->_post['id']);
DB::Aowow()->query('UPDATE ?_account_weightscales SET `icon`= ? WHERE `id` = ?d', $this->_post['__icon'], $this->_post['id']);
foreach (explode(',', $this->_post['scale']) as $s)
{
[$k, $v] = explode(':', $s);
if (!in_array($k, Util::$weightScales) || $v < 1)
continue;
if (DB::Aowow()->query('INSERT INTO ?_account_weightscale_data VALUES (?d, ?, ?d)', $this->_post['id'], $k, $v) === null)
{
trigger_error('AdminWeightpresetsActionSaveResponse - failed to write to database', E_USER_ERROR);
$this->result = self::ERR_WRITE_DB;
return;
}
}
// write dataset
exec('php aowow --build=weightPresets', $out);
foreach ($out as $o)
if (strstr($o, 'ERR'))
{
trigger_error('AdminWeightpresetsActionSaveResponse - failed to write dataset' . $o, E_USER_ERROR);
$this->result = self::ERR_WRITE_FILE;
return;
}
// all done
$this->result = self::ERR_NONE;
}
protected static function checkScale(string $val) : string
{
if (preg_match('/^((\w+:\d+)(,\w+:\d+)*)$/', $val))
return $val;
return '';
}
}
?>

View File

@@ -0,0 +1,141 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AreatriggerBaseResponse extends TemplateResponse implements ICache
{
use TrDetailPage, TrCache;
protected int $cacheType = CACHE_TYPE_DETAIL_PAGE;
protected int $requiredUserGroup = U_GROUP_STAFF;
protected string $template = 'detail-page-generic';
protected string $pageName = 'areatrigger';
protected ?int $activeTab = parent::TAB_DATABASE;
protected array $breadcrumb = [0, 102];
public int $type = Type::AREATRIGGER;
public int $typeId = 0;
private AreaTriggerList $subject;
public function __construct(string $id)
{
parent::__construct($id);
$this->typeId = intVal($id);
$this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE;
}
protected function generate() : void
{
$this->subject = new AreaTriggerList(array(['id', $this->typeId]));
if ($this->subject->error)
$this->generateNotFound(Lang::game('areatrigger'), Lang::areatrigger('notFound'));
$this->h1 = $this->subject->getField('name') ?: 'Areatrigger #'.$this->typeId;
$this->gPageInfo += array(
'type' => $this->type,
'typeId' => $this->typeId,
'name' => $this->h1
);
/*************/
/* Menu Path */
/*************/
$this->breadcrumb[] = $this->subject->getField('type');
/**************/
/* Page Title */
/**************/
array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('areatrigger')));
/****************/
/* Main Content */
/****************/
$_type = $this->subject->getField('type');
// get spawns
if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL))
{
$this->addDataLoader('zones');
$this->map = array(
['parent' => 'mapper-generic'], // Mapper
$spawns, // mapperData
null, // ShowOnMap
[Lang::areatrigger('foundIn')] // foundIn
);
foreach ($spawns as $areaId => $_)
$this->map[3][$areaId] = ZoneList::getName($areaId);
}
// Smart AI
if ($_type == AT_TYPE_SMART)
{
$sai = new SmartAI(SmartAI::SRC_TYPE_AREATRIGGER, $this->typeId, ['teleportTargetArea' => $this->subject->getField('areaId')]);
if ($sai->prepare())
{
$this->extendGlobalData($sai->getJSGlobals());
$this->smartAI = $sai->getMarkup();
}
}
$this->redButtons = array(
BUTTON_LINKS => false,
BUTTON_WOWHEAD => false
);
/**************/
/* Extra Tabs */
/**************/
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true);
// tab: conditions
$cnd = new Conditions();
$cnd->getBySource(Conditions::SRC_AREATRIGGER_CLIENT, entry: $this->typeId)->prepare();
if ($tab = $cnd->toListviewTab())
{
$this->extendGlobalData($cnd->getJsGlobals());
$this->lvTabs->addDataTab(...$tab);
}
if ($_type == AT_TYPE_OBJECTIVE)
{
$relQuest = new QuestList(array(['id', $this->subject->getField('quest')]));
if (!$relQuest->error)
{
$this->extendGlobalData($relQuest->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS));
$this->lvTabs->addListviewTab(new Listview(['data' => $relQuest->getListviewData()], QuestList::$brickFile));
}
}
else if ($_type == AT_TYPE_TELEPORT)
{
$relZone = new ZoneList(array(['id', $this->subject->getField('areaId')]));
if (!$relZone->error)
$this->lvTabs->addListviewTab(new Listview(['data' => $relZone->getListviewData()], ZoneList::$brickFile));
}
else if ($_type == AT_TYPE_SCRIPT)
{
$relTrigger = new AreaTriggerList(array(['id', $this->typeId, '!'], ['name', $this->subject->getField('name')]));
if (!$relTrigger->error)
$this->lvTabs->addListviewTab(new Listview(['data' => $relTrigger->getListviewData(), 'name' => Util::ucFirst(Lang::game('areatrigger'))]), AreaTriggerList::$brickFile, 'areatrigger');
}
parent::generate();
}
}
?>

View File

@@ -0,0 +1,102 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class AreatriggersBaseResponse extends TemplateResponse implements ICache
{
use TrListPage, TrCache;
protected int $type = Type::AREATRIGGER;
protected int $cacheType = CACHE_TYPE_LIST_PAGE;
protected int $requiredUserGroup = U_GROUP_STAFF;
protected string $template = 'areatriggers';
protected string $pageName = 'areatriggers';
protected ?int $activeTab = parent::TAB_DATABASE;
protected array $breadcrumb = [0, 102];
protected array $scripts = [[SC_JS_FILE, 'js/filters.js']];
protected array $expectedGET = ['filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]]];
protected array $validCats = [0, 1, 2, 3, 4, 5];
public function __construct(string $pageParam)
{
$this->getCategoryFromUrl($pageParam);
if (isset($this->category[0]))
$this->forward('?areatriggers&filter=ty='.$this->category[0]);
parent::__construct($pageParam);
$this->filter = new AreaTriggerListFilter($this->_get['filter'] ?? '');
if ($this->filter->shouldReload)
{
$_SESSION['error']['fi'] = $this->filter::class;
$get = $this->filter->buildGETParam();
$this->forward('?' . $this->pageName . ($get ? '&filter=' . $get : ''));
}
$this->filterError = $this->filter->error;
}
protected function generate() : void
{
$this->h1 = Util::ucFirst(Lang::game('areatriggers'));
$fiForm = $this->filter->values;
/**************/
/* Page Title */
/**************/
array_unshift($this->title, $this->h1);
if (count($fiForm['ty']) == 1)
array_unshift($this->title, Lang::areatrigger('types', $fiForm['ty'][0]));
/*************/
/* Menu Path */
/*************/
if (count($fiForm['ty']) == 1)
$this->breadcrumb[] = $fiForm['ty'];
/****************/
/* Main Content */
/****************/
$this->redButtons[BUTTON_WOWHEAD] = false;
$conditions = [];
if ($_ = $this->filter->getConditions())
$conditions[] = $_;
$tabData = [];
$trigger = new AreaTriggerList($conditions, ['calcTotal' => true]);
if (!$trigger->error)
{
$tabData['data'] = $trigger->getListviewData();
// create note if search limit was exceeded; overwriting 'note' is intentional
if ($trigger->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT'))
{
$tabData['note'] = sprintf(Util::$tryFilteringEntityString, $trigger->getMatches(), '"'.Lang::game('areatriggers').'"', Cfg::get('SQL_LIMIT_DEFAULT'));
$tabData['_truncated'] = 1;
}
}
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]);
$this->lvTabs->addListviewTab(new Listview($tabData, AreaTriggerList::$brickFile, 'areatrigger'));
parent::generate();
}
}
?>

View File

@@ -0,0 +1,149 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class ArenateamBaseResponse extends TemplateResponse
{
use TrProfilerDetail;
protected string $template = 'roster';
protected string $pageName = 'arena-team';
protected ?int $activeTab = parent::TAB_TOOLS;
protected array $breadcrumb = [1, 5, 3]; // Tools > Profiler > Arena Team
protected array $dataLoader = ['realms', 'weight-presets'];
protected array $scripts = array(
[SC_JS_FILE, 'js/profile_all.js'],
[SC_JS_FILE, 'js/profile.js'],
[SC_CSS_FILE, 'css/Profiler.css']
);
public int $type = Type::ARENA_TEAM;
public function __construct(string $idOrProfile)
{
parent::__construct($idOrProfile);
if (!Cfg::get('PROFILER_ENABLE'))
$this->generateError();
if (!$idOrProfile)
$this->generateError();
$this->getSubjectFromUrl($idOrProfile);
// we have an ID > ok
if ($this->typeId)
return;
// param was incomplete profile > error
if (!$this->subjectName)
$this->generateError();
// 3 possibilities
// 1) already synced to aowow
if ($subject = DB::Aowow()->selectRow('SELECT `id`, `realmGUID`, `stub` FROM ?_profiler_arena_team WHERE `realm` = ?d AND `nameUrl` = ?', $this->realmId, Profiler::urlize($this->subjectName)))
{
$this->typeId = $subject['id'];
if ($subject['stub'])
$this->handleIncompleteData(Type::ARENA_TEAM, $subject['realmGUID']);
return;
}
// 2) not yet synced but exists on realm (wont work if we get passed an urlized name, but there is nothing we can do about it)
$subjects = DB::Characters($this->realmId)->select('SELECT at.`arenaTeamId` AS "realmGUID", at.`name`, at.`type` FROM arena_team at WHERE at.`name` = ?', $this->subjectName);
if ($subject = array_filter($subjects, fn($x) => Util::lower($x['name']) === Util::lower($this->subjectName)))
{
$subject = array_pop($subject);
$subject['realm'] = $this->realmId;
$subject['stub'] = 1;
$subject['nameUrl'] = Profiler::urlize($subject['name']);
// create entry from realm with basic info
DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_arena_team (?#) VALUES (?a)', array_keys($subject), array_values($subject));
$this->handleIncompleteData(Type::ARENA_TEAM, $subject['realmGUID']);
return;
}
// 3) does not exist at all
$this->notFound();
}
protected function generate() : void
{
if ($this->doResync)
{
parent::generate();
return;
}
$subject = new LocalArenaTeamList(array(['at.id', $this->typeId]));
if ($subject->error)
$this->notFound();
// arena team accessed by id
if (!$this->subjectName)
$this->forward($subject->getProfileUrl());
$this->h1 = Lang::profiler('arenaRoster', [$subject->getField('name')]);
/*************/
/* Menu Path */
/*************/
$this->followBreadcrumbPath();
/**************/
/* Page Title */
/**************/
array_unshift(
$this->title,
$subject->getField('name').' ('.$this->realm.' - '.Lang::profiler('regions', $this->region).')',
Util::ucFirst(Lang::profiler('profiler'))
);
/****************/
/* Main Content */
/****************/
parent::generate();
$this->redButtons[BUTTON_RESYNC] = [$this->typeId, 'arena-team'];
// statistic calculations here
/**************/
/* Extra Tabs */
/**************/
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated');
// tab: members
$member = new LocalProfileList(array(['atm.arenaTeamId', $this->typeId]));
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $member->getListviewData(PROFILEINFO_CHARACTER | PROFILEINFO_ARENA),
'sort' => [-15],
'visibleCols' => ['race', 'classs', 'level', 'talents', 'gearscore', 'rating', 'wins', 'losses'],
'hiddenCols' => ['guild', 'location']
), ProfileList::$brickFile));
}
private function notFound() : never
{
parent::generateNotFound(Lang::game('arenateam'), Lang::profiler('notFound', 'arenateam'));
}
}
?>

View File

@@ -0,0 +1,48 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class ArenaTeamResyncResponse extends TextResponse
{
protected array $expectedGET = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'] ],
'profile' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']]
);
public function __construct(string $pageParam)
{
parent::__construct($pageParam);
if (!Cfg::get('PROFILER_ENABLE'))
$this->generate404();
}
/* params
id: <prId1,prId2,..,prIdN>
user: <string> [optional, not used]
profile: <empty> [optional, also get related chars]
return: 1
*/
protected function generate() : void
{
if (!$this->assertGET('id'))
return;
if ($teams = DB::Aowow()->select('SELECT `realm`, `realmGUID` FROM ?_profiler_arena_team WHERE `id` IN (?a)', $this->_get['id']))
foreach ($teams as $t)
Profiler::scheduleResync(Type::ARENA_TEAM, $t['realm'], $t['realmGUID']);
if ($this->_get['profile'])
if ($chars = DB::Aowow()->select('SELECT `realm`, `realmGUID` FROM ?_profiler_profiles p JOIN ?_profiler_arena_team_member atm ON atm.`profileId` = p.`id` WHERE atm.`arenaTeamId` IN (?a)', $this->_get['id']))
foreach ($chars as $c)
Profiler::scheduleResync(Type::PROFILE, $c['realm'], $c['realmGUID']);
$this->result = 1; // as string?
}
}
?>

View File

@@ -0,0 +1,29 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class ArenaTeamStatusResponse extends TextResponse
{
protected array $expectedGET = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList']]
);
public function __construct(string $pageParam)
{
parent::__construct($pageParam);
if (!Cfg::get('PROFILER_ENABLE'))
$this->generate404();
}
protected function generate() : void
{
$this->result = Profiler::resyncStatus(Type::ARENA_TEAM, $this->_get['id']);
}
}
?>

View File

@@ -0,0 +1,158 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class ArenateamsBaseResponse extends TemplateResponse implements IProfilerList
{
use TrProfilerList, TrListPage;
protected string $template = 'arena-teams';
protected string $pageName = 'arena-teams';
protected ?int $activeTab = parent::TAB_TOOLS;
protected array $breadcrumb = [1, 5, 3]; // Tools > Profiler > Arena Teams
protected array $dataLoader = ['realms'];
protected array $scripts = array(
[SC_JS_FILE, 'js/filters.js'],
[SC_JS_FILE, 'js/profile_all.js'],
[SC_JS_FILE, 'js/profile.js']
);
protected array $expectedGET = array(
'filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]]
);
public int $type = Type::ARENA_TEAM;
private int $sumSubjects = 0;
public function __construct(string $pageParam)
{
if (!Cfg::get('PROFILER_ENABLE'))
$this->generateError();
$this->getSubjectFromUrl($pageParam);
parent::__construct($pageParam);
$realms = [];
foreach (Profiler::getRealms() as $idx => $r)
{
if ($this->region && $r['region'] != $this->region)
continue;
if ($this->realm && $r['name'] != $this->realm)
continue;
$this->sumSubjects += DB::Characters($idx)->selectCell('SELECT count(*) FROM arena_team');
$realms[] = $idx;
}
$this->subCat = $pageParam !== '' ? '='.$pageParam : '';
$this->filter = new ArenaTeamListFilter($this->_get['filter'] ?? '', ['realms' => $realms]);
if ($this->filter->shouldReload)
{
$_SESSION['error']['fi'] = $this->filter::class;
$get = $this->filter->buildGETParam();
$this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : ''));
}
$this->filterError = $this->filter->error;
}
protected function generate() : void
{
$this->h1 = Lang::game('arenateams');
/*************/
/* Menu Path */
/*************/
$this->followBreadcrumbPath();
/**************/
/* Page Title */
/**************/
if ($this->realm)
array_unshift($this->title, $this->realm,/* Cfg::get('BATTLEGROUP'),*/ Lang::profiler('regions', $this->region), Lang::game('arenateams'));
else if ($this->region)
array_unshift($this->title, Lang::profiler('regions', $this->region), Lang::game('arenateams'));
else
array_unshift($this->title, Lang::game('arenateams'));
/****************/
/* Main Content */
/****************/
$conditions = [];
if (!User::isInGroup(U_GROUP_EMPLOYEE))
$conditions[] = ['at.seasonGames', 0, '>'];
if ($_ = $this->filter->getConditions())
$conditions[] = $_;
$this->getRegions();
$tabData = array(
'id' => 'arena-teams',
'data' => [],
'hideCount' => 1,
'sort' => [-16],
'extraCols' => ['$Listview.extraCols.members'],
'visibleCols' => ['rank', 'wins', 'losses', 'rating'],
'hiddenCols' => ['arenateam', 'guild']
);
if (!$this->filter->values['sz'])
$tabData['visibleCols'][] = 'size';
if ($this->filter->values['si'])
$tabData['hiddenCols'][] = 'faction';
$miscParams = ['calcTotal' => true];
if ($this->realm)
$miscParams['sv'] = $this->realm;
if ($this->region)
$miscParams['rg'] = $this->region;
$teams = new RemoteArenaTeamList($conditions, $miscParams);
if (!$teams->error)
{
$teams->initializeLocalEntries();
$tabData['data'] = $teams->getListviewData();
// create note if search limit was exceeded
if ($this->filter->query && $teams->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT'))
{
$tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_arenateamsfound2', $this->sumSubjects, $teams->getMatches());
$tabData['_truncated'] = 1;
}
else if ($teams->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT'))
$tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_arenateamsfound', $this->sumSubjects, 0);
}
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated');
$this->lvTabs->addListviewTab(new Listview($tabData, ArenaTeamList::$brickFile, 'membersCol'));
parent::generate();
$this->result->registerDisplayHook('filter', [self::class, 'filterFormHook']);
}
public static function filterFormHook(Template\PageTemplate &$pt, ArenaTeamListFilter $filter) : void
{
// sort for dropdown-menus
Lang::sort('game', 'cl');
Lang::sort('game', 'ra');
}
}
?>

277
endpoints/class/class.php Normal file
View File

@@ -0,0 +1,277 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class ClassBaseResponse extends TemplateResponse implements ICache
{
use TrDetailPage, TrCache;
private const TC_CLASS_IDS = [null, 8, 3, 1, 5, 4, 9, 6, 2, 7, null, 0]; // see TalentCalc.js
protected int $cacheType = CACHE_TYPE_DETAIL_PAGE;
protected string $template = 'detail-page-generic';
protected string $pageName = 'class';
protected ?int $activeTab = parent::TAB_DATABASE;
protected array $breadcrumb = [0, 12];
public int $type = Type::CHR_CLASS;
public int $typeId = 0;
public ?string $expansion = null;
private CharClassList $subject;
public function __construct(string $id)
{
parent::__construct($id);
$this->typeId = intVal($id);
$this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE;
}
protected function generate() : void
{
$this->subject = new CharClassList(array(['id', $this->typeId]));
if ($this->subject->error)
$this->generateNotFound(Lang::game('class'), Lang::chrClass('notFound'));
$this->h1 = $this->subject->getField('name', true);
$this->gPageInfo += array(
'type' => $this->type,
'typeId' => $this->typeId,
'name' => $this->h1
);
/*************/
/* Menu Path */
/*************/
$this->breadcrumb[] = $this->typeId;
/**************/
/* Page Title */
/**************/
array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('class')));
/***********/
/* Infobox */
/***********/
$cl = ChrClass::from($this->typeId);
$infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags'));
// hero class
if ($this->subject->getField('flags') & 0x40)
$infobox[] = '[tooltip=tooltip_heroclass]'.Lang::game('heroClass').'[/tooltip]';
// resource
if ($cl == ChrClass::DRUID) // special Druid case
$infobox[] = Lang::game('resources').
'[tooltip name=powertype1]'.Lang::game('st', 0).', '.Lang::game('st', 31).', '.Lang::game('st', 2).'[/tooltip][span class=tip tooltip=powertype1]'.Util::ucFirst(Lang::spell('powerTypes', POWER_MANA)).'[/span], '.
'[tooltip name=powertype2]'.Lang::game('st', 5).', '.Lang::game('st', 8).'[/tooltip][span class=tip tooltip=powertype2]'.Util::ucFirst(Lang::spell('powerTypes', POWER_RAGE)).'[/span], '.
'[tooltip name=powertype8]'.Lang::game('st', 1).'[/tooltip][span class=tip tooltip=powertype8]'.Util::ucFirst(Lang::spell('powerTypes', POWER_ENERGY)).'[/span]';
else if ($cl == ChrClass::DEATHKNIGHT) // special DK case
$infobox[] = Lang::game('resources').'[span]'.Util::ucFirst(Lang::spell('powerTypes', POWER_RUNE)).', '.Util::ucFirst(Lang::spell('powerTypes', $this->subject->getField('powerType'))).'[/span]';
else // regular case
$infobox[] = Lang::game('resource').'[span]'.Util::ucFirst(Lang::spell('powerTypes', $this->subject->getField('powerType'))).'[/span]';
// roles
$roles = [];
for ($i = 0; $i < 4; $i++)
if ($this->subject->getField('roles') & (1 << $i))
$roles[] = (count($roles) == 2 ? "[br]" : '').Lang::game('_roles', $i);
if ($roles)
$infobox[] = (count($roles) > 1 ? Lang::game('roles') : Lang::game('role')).implode(', ', $roles);
// specs
$specList = [];
$skills = new SkillList(array(['id', $this->subject->getField('skills')]));
foreach ($skills->iterate() as $k => $__)
$specList[$k] = '[icon name='.$skills->getField('iconString').'][url=?spells=7.'.$this->typeId.'.'.$k.']'.$skills->getField('name', true).'[/url][/icon]';
if ($specList)
$infobox[] = Lang::game('specs').'[ul][li]'.implode('[/li][li]', $specList).'[/li][/ul]';
// id
$infobox[] = Lang::chrClass('id') . $this->typeId;
// original name
if (Lang::getLocale() != Locale::EN)
$infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]';
if ($infobox)
$this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0');
/****************/
/* Main Content */
/****************/
$this->expansion = Util::$expansionString[$this->subject->getField('expansion')];
$this->headIcons = ['class_'.$cl->json()];
$this->redButtons = array(
BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId],
BUTTON_WOWHEAD => true,
BUTTON_TALENT => ['href' => '?talent#'.Util::$tcEncoding[self::TC_CLASS_IDS[$this->typeId] * 3], 'pet' => false],
BUTTON_FORUM => false // todo (low): Cfg::get('BOARD_URL') + X
);
/**************/
/* Extra Tabs */
/**************/
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true);
// Tab: Spells (grouped)
// '$LANG.tab_armorproficiencies',
// '$LANG.tab_weaponskills',
// '$LANG.tab_glyphs',
// '$LANG.tab_abilities',
// '$LANG.tab_talents',
$conditions = array(
['s.typeCat', [-13, -11, -2, 7]],
[['s.cuFlags', (SPELL_CU_TRIGGERED | CUSTOM_EXCLUDE_FOR_LISTVIEW), '&'], 0],
[
'OR',
// Glyphs, Proficiencies
['s.reqClassMask', $cl->toMask(), '&'],
// Abilities / Talents
['s.skillLine1', $this->subject->getField('skills')],
['AND', ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->subject->getField('skills')]]
],
[ // last rank or unranked
'OR',
['s.cuFlags', SPELL_CU_LAST_RANK, '&'],
['s.rankNo', 0]
],
Cfg::get('SQL_LIMIT_NONE')
);
$genSpells = new SpellList($conditions);
if (!$genSpells->error)
{
$this->extendGlobalData($genSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $genSpells->getListviewData(),
'id' => 'spells',
'name' => '$LANG.tab_spells',
'visibleCols' => ['level', 'schools', 'type', 'classes'],
'hiddenCols' => ['reagents', 'skill'],
'sort' => ['-level', 'type', 'name'],
'computeDataFunc' => '$Listview.funcBox.initSpellFilter',
'onAfterCreate' => '$Listview.funcBox.addSpellIndicator'
), SpellList::$brickFile));
}
// Tab: Items (grouped)
$conditions = array(
['requiredClass', 0, '>'],
['requiredClass', $cl->toMask(), '&'],
[['requiredClass', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!'],
['itemset', 0],
Cfg::get('SQL_LIMIT_NONE')
);
$items = new ItemList($conditions);
if (!$items->error)
{
$this->extendGlobalData($items->getJSGlobals());
$hiddenCols = null;
if ($items->hasDiffFields('requiredRace'))
$hiddenCols = ['side'];
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $items->getListviewData(),
'id' => 'items',
'name' => '$LANG.tab_items',
'visibleCols' => ['dps', 'armor', 'slot'],
'hiddenCols' => $hiddenCols,
'computeDataFunc' => '$Listview.funcBox.initSubclassFilter',
'onAfterCreate' => '$Listview.funcBox.addSubclassIndicator',
'note' => sprintf(Util::$filterResultString, '?items&filter=cr=152;crs='.$this->typeId.';crv=0'),
'_truncated' => 1
), ItemList::$brickFile));
}
// Tab: Quests
$conditions = array(
['reqClassMask', $cl->toMask(), '&'],
[['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!']
);
$quests = new QuestList($conditions);
if (!$quests->error)
{
$this->extendGlobalData($quests->getJSGlobals());
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $quests->getListviewData(),
'sort' => ['reqlevel', 'name']
), QuestList::$brickFile));
}
// Tab: Itemsets
$sets = new ItemsetList(array(['classMask', $cl->toMask(), '&']));
if (!$sets->error)
{
$this->extendGlobalData($sets->getJSGlobals(GLOBALINFO_SELF));
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $sets->getListviewData(),
'note' => sprintf(Util::$filterResultString, '?itemsets&filter=cl='.$this->typeId),
'hiddenCols' => ['classes'],
'sort' => ['-level', 'name']
), ItemsetList::$brickFile));
}
// Tab: Trainer
$conditions = array(
['npcflag', NPC_FLAG_TRAINER | NPC_FLAG_CLASS_TRAINER, '&'],
['trainerType', 0], // trains class spells
['trainerRequirement', $this->typeId]
);
$trainer = new CreatureList($conditions);
if (!$trainer->error)
{
$this->addDataLoader('zones');
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $trainer->getListviewData(),
'id' => 'trainers',
'name' => '$LANG.tab_trainers'
), CreatureList::$brickFile));
}
// Tab: Races
$races = new CharRaceList(array(['classMask', $cl->toMask(), '&']));
if (!$races->error)
$this->lvTabs->addListviewTab(new Listview(['data' => $races->getListviewData()], CharRaceList::$brickFile));
// tab: condition-for
$cnd = new Conditions();
$cnd->getByCondition(Type::CHR_CLASS, $this->typeId)->prepare();
if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for'))
{
$this->extendGlobalData($cnd->getJsGlobals());
$this->lvTabs->addDataTab(...$tab);
}
parent::generate();
}
}
?>

View File

@@ -0,0 +1,48 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class ClassesBaseResponse extends TemplateResponse implements ICache
{
use TrListPage, TrCache;
protected int $type = Type::CHR_CLASS;
protected int $cacheType = CACHE_TYPE_LIST_PAGE;
protected string $template = 'list-page-generic';
protected string $pageName = 'classes';
protected ?int $activeTab = parent::TAB_DATABASE;
protected array $breadcrumb = [0, 12];
public function __construct(string $pageParam)
{
$this->getCategoryFromUrl($pageParam);
parent::__construct($pageParam);
}
protected function generate() : void
{
$this->h1 = Util::ucFirst(Lang::game('classes'));
array_unshift($this->title, Util::ucFirst(Lang::game('classes')));
$this->redButtons[BUTTON_WOWHEAD] = true;
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]);
$classes = new CharClassList();
if (!$classes->error)
$this->lvTabs->addListviewTab(new Listview(['data' => $classes->getListviewData()], CharClassList::$brickFile));
parent::generate();
}
}
?>

View File

@@ -0,0 +1,51 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// returns all replies on success
// must have non-200 header on error
class CommentAddreplyResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'commentId' => ['filter' => FILTER_VALIDATE_INT ],
'replyId' => ['filter' => FILTER_VALIDATE_INT ],
'body' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']]
);
protected function generate(): void
{
if (!$this->assertPOST('commentId', 'replyId', 'body'))
{
trigger_error('CommentAddreplyResponse - malformed request received', E_USER_ERROR);
$this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : '');
}
if (!User::canReply())
$this->generate404(Lang::main('cannotComment'));
if (!$this->_post['commentId'] || !DB::Aowow()->selectCell('SELECT 1 FROM ?_comments WHERE `id` = ?d', $this->_post['commentId']))
{
trigger_error('CommentAddreplyResponse - parent comment #'.$this->_post['commentId'].' does not exist', E_USER_ERROR);
$this->generate404(Lang::main('intError'));
}
if (mb_strlen($this->_post['body']) < CommunityContent::REPLY_LENGTH_MIN || mb_strlen($this->_post['body']) > CommunityContent::REPLY_LENGTH_MAX)
$this->generate404(Lang::main('textLength', [mb_strlen($this->_post['body']), CommunityContent::REPLY_LENGTH_MIN, CommunityContent::REPLY_LENGTH_MAX]));
if (!DB::Aowow()->query('INSERT INTO ?_comments (`userId`, `roles`, `body`, `date`, `replyTo`) VALUES (?d, ?d, ?, UNIX_TIMESTAMP(), ?d)', User::$id, User::$groups, $this->_post['body'], $this->_post['commentId']))
{
trigger_error('CommentAddreplyResponse - write to db failed', E_USER_ERROR);
$this->generate404(Lang::main('intError'));
}
$this->result = Util::toJSON(CommunityContent::getCommentReplies($this->_post['commentId']));
}
}
?>

79
endpoints/comment/add.php Normal file
View File

@@ -0,0 +1,79 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class CommentAddResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'commentbody' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']]
);
protected array $expectedGET = array(
'type' => ['filter' => FILTER_VALIDATE_INT],
'typeid' => ['filter' => FILTER_VALIDATE_INT]
);
// i .. have problems believing, that everything uses nifty ajax while adding comments requires a brutal header(Loacation: <wherever>), yet, thats how it is
protected function generate() : void
{
if (!$this->assertGET('type', 'typeid') || !$this->assertPOST('commentbody') || !Type::validateIds($this->_get['type'], $this->_get['typeid']))
{
trigger_error('CommentAddResponse - malforemd request received', E_USER_ERROR);
return; // whatever, we cant even send him back
}
// we now have a valid return target
$idOrUrl = $this->_get['typeid'];
if ($this->_get['type'] == Type::GUIDE)
if ($_ = DB::Aowow()->selectCell('SELECT `url` FROM ?_guides WHERE `id` = ?d', $this->_get['typeid']))
$idOrUrl = $_;
$this->redirectTo = '?'.Type::getFileString($this->_get['type']).'='.$idOrUrl.'#comments';
// this type cannot be commented on
if (!Type::checkClassAttrib($this->_get['type'], 'contribute', CONTRIBUTE_CO))
{
trigger_error('CommentAddResponse - tried to comment on unsupported type: '.Type::getFileString($this->_get['type']), E_USER_ERROR);
$_SESSION['error']['co'] = Lang::main('intError');
return;
}
if (!User::canComment())
{
$_SESSION['error']['co'] = Lang::main('cannotComment');
return;
}
$len = mb_strlen($this->_post['commentbody']);
if ((!User::isInGroup(U_GROUP_MODERATOR) && $len < CommunityContent::COMMENT_LENGTH_MIN) || ($len > CommunityContent::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1)))
{
$_SESSION['error']['co'] = Lang::main('textLength', [$len, CommunityContent::COMMENT_LENGTH_MIN, CommunityContent::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1)]);
return;
}
if ($postId = DB::Aowow()->query('INSERT INTO ?_comments (`type`, `typeId`, `userId`, `roles`, `body`, `date`) VALUES (?d, ?d, ?d, ?d, ?, UNIX_TIMESTAMP())', $this->_get['type'], $this->_get['typeid'], User::$id, User::$groups, $this->_post['commentbody']))
{
Util::gainSiteReputation(User::$id, SITEREP_ACTION_COMMENT, ['id' => $postId]);
// every comment starts with a rating of +1 and i guess the simplest thing to do is create a db-entry with the system as owner
DB::Aowow()->query('INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, 0, 1)', RATING_COMMENT, $postId);
// flag target with hasComment
if ($tbl = Type::getClassAttrib($this->_get['type'], 'dataTable'))
DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_COMMENT, $this->_get['typeid']);
return;
}
trigger_error('CommentAddResponse - write to db failed', E_USER_ERROR);
$_SESSION['error']['co'] = Lang::main('intError');
}
}
?>

View File

@@ -0,0 +1,44 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// expects non-200 header on error
class CommentDeletereplyResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_VALIDATE_INT]
);
protected function generate() : void
{
if (!$this->assertPOST('id'))
{
trigger_error('CommentDeletereplyResponse - malformed request received', E_USER_ERROR);
$this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : '');
}
// flag as deleted (unset sticky (can a reply even be sticky?)
$ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d | ?d, `deleteUserId` = ?d, `deleteDate` = UNIX_TIMESTAMP() WHERE `id` = ?d { AND `userId` = ?d }',
CC_FLAG_STICKY, CC_FLAG_DELETED,
User::$id,
$this->_post['id'],
User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id
);
if ($ok)
DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d', RATING_COMMENT, $this->_post['id']);
else
{
trigger_error('CommentDeletereplyResponse - deleting reply #'.$this->_post['id'].' by user #'.User::$id.' from db failed', E_USER_ERROR);
$this->generate404(Lang::main('intError'));
}
}
}
?>

View File

@@ -0,0 +1,53 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class CommentDeleteResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']],
// 'username' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ]
);
protected function generate() : void
{
if (!$this->assertPOST('id'))
{
trigger_error('CommentDeleteResponse - malformed request received', E_USER_ERROR);
return;
}
// in theory, there is a username passed alongside if executed from userpage... lets just use the current user (see user.js)
$ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d, `deleteUserId` = ?d, `deleteDate` = UNIX_TIMESTAMP() WHERE `id` IN (?a) { AND `userId` = ?d }',
CC_FLAG_DELETED,
User::$id,
$this->_post['id'],
User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id
);
// unflag subject: hasComment
if ($ok)
{
$coInfo = DB::Aowow()->select(
'SELECT IF(BIT_OR(~b.`flags`) & ?d, 1, 0) AS "0", b.`type` AS "1", b.`typeId` AS "2" FROM ?_comments a JOIN ?_comments b ON a.`type` = b.`type` AND a.`typeId` = b.`typeId` WHERE a.`id` IN (?a) GROUP BY b.`type`, b.`typeId`',
CC_FLAG_DELETED, $this->_post['id']
);
foreach ($coInfo as [$hasMore, $type, $typeId])
if (!$hasMore && ($tbl = Type::getClassAttrib($type, 'dataTable')))
DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` & ~?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_COMMENT, $typeId);
return;
}
trigger_error('CommentDeleteResponse - user #'.User::$id.' could not flag comment(s) #'.implode(', ', $this->_post['id']).' as deleted', E_USER_ERROR);
}
}
?>

View File

@@ -0,0 +1,30 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// expects non-200 header on error
class CommentDetachreplyResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_MODERATOR;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_VALIDATE_INT]
);
protected function generate() : void
{
if (!$this->assertPOST('id'))
{
trigger_error('CommentDetachreplyResponse - malformed request received', E_USER_ERROR);
$this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : '');
}
DB::Aowow()->query('UPDATE ?_comments c1, ?_comments c2 SET c1.`replyTo` = 0, c1.`type` = c2.`type`, c1.`typeId` = c2.`typeId` WHERE c1.`replyTo` = c2.`id` AND c1.`id` = ?d', $this->_post['id']);
}
}
?>

View File

@@ -0,0 +1,61 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// expects non-200 header on error
class CommentDownvotereplyResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_VALIDATE_INT]
);
protected function generate() : void
{
if (!$this->assertPOST('id'))
{
trigger_error('CommentDownvotereplyResponse - malformed request received', E_USER_ERROR);
$this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : '');
}
if (!User::canDownvote())
$this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'cannot downvote' : '');
$comment = DB::Aowow()->selectRow('SELECT `userId`, IF(`flags` & ?d, 1, 0) AS "deleted" FROM ?_comments WHERE `id` = ?d', CC_FLAG_DELETED, $this->_post['id']);
if (!$comment)
{
trigger_error('CommentDownvotereplyResponse - comment #'.$this->_post['id'].' not found in db', E_USER_ERROR);
$this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'replyID not found' : '');
}
if (User::$id == $comment['userId']) // not worth logging?
$this->generate404('LANG.voteself_tip');
if ($comment['deleted'])
$this->generate404('LANG.votedeleted_tip');
$ok = DB::Aowow()->query(
'INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)',
RATING_COMMENT,
$this->_post['id'],
User::$id,
User::canSupervote() ? -2 : -1
);
if (!is_int($ok))
{
trigger_error('CommentDownvotereplyResponse - write to db failed', E_USER_ERROR);
$this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'write to db failed' : '');
}
Util::gainSiteReputation($comment['userId'], SITEREP_ACTION_DOWNVOTED, ['id' => $this->_post['id'], 'voterId' => User::$id]);
User::decrementDailyVotes();
}
}
?>

View File

@@ -0,0 +1,62 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// returns all replies on success
// must have non-200 header on error
class CommentEditreplyResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'commentId' => ['filter' => FILTER_VALIDATE_INT ],
'replyId' => ['filter' => FILTER_VALIDATE_INT ],
'body' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']]
);
protected function generate() : void
{
if (!$this->assertPOST('commentId', 'replyId', 'body'))
{
trigger_error('CommentEditreplyResponse - malformed request received', E_USER_ERROR);
$this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : '');
}
$ownerId = DB::Aowow()->selectCell('SELECT `userId` FROM ?_comments WHERE `id` = ?d AND `replyTo` = ?d', $this->_post['replyId'], $this->_post['commentId']);
if (!User::canReply() || (User::$id != $ownerId && !User::isInGroup(U_GROUP_MODERATOR)))
$this->generate404(Lang::main('cannotComment'));
if (!$ownerId)
{
trigger_error('CommentEditreplyResponse - comment #'.$this->_post['commentId'].' or reply #'.$this->_post['replyId'].' does not exist', E_USER_ERROR);
$this->generate404(Lang::main('intError'));
}
if (mb_strlen($this->_post['body']) < CommunityContent::REPLY_LENGTH_MIN || mb_strlen($this->_post['body']) > CommunityContent::REPLY_LENGTH_MAX)
$this->generate404(Lang::main('textLength', [mb_strlen($this->_post['body']), CommunityContent::REPLY_LENGTH_MIN, CommunityContent::REPLY_LENGTH_MAX]));
$update = array(
'body' => $this->_post['body'],
'editUserId' => User::$id,
'editDate' => time()
);
if (User::$id == $ownerId)
$update['roles'] = User::$groups;
if (!DB::Aowow()->query('UPDATE ?_comments SET `editCount` = `editCount` + 1, ?a WHERE `id` = ?d AND `replyTo` = ?d { AND `userId` = ?d }',
$update, $this->_post['replyId'], $this->_post['commentId'], User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id))
{
trigger_error('CommentEditreplyResponse - write to db failed', E_USER_ERROR);
$this->generate404(Lang::main('intError'));
}
$this->result = Util::toJSON(CommunityContent::getCommentReplies($this->_post['commentId']));
}
}
?>

View File

@@ -0,0 +1,63 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class CommentEditResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'body' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']],
'response' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']]
);
protected array $expectedGET = array(
'id' => ['filter' => FILTER_VALIDATE_INT]
);
protected function generate() : void
{
if (!$this->assertGET('id') || !$this->assertPOST('body'))
{
trigger_error('CommentEditResponse - malforemd request received', E_USER_ERROR);
return;
}
$ownerId = DB::Aowow()->selectCell('SELECT `userId` FROM ?_comments WHERE `id` = ?d', $this->_get['id']);
if (!User::canComment() || (User::$id != $ownerId && !User::isInGroup(U_GROUP_MODERATOR)))
{
trigger_error('CommentEditResponse - user #'.User::$id.' not allowed to edit comment #'.$this->_get['id'], E_USER_ERROR);
return;
}
if (!User::isInGroup(U_GROUP_MODERATOR) && mb_strlen($this->_post['body']) < CommunityContent::COMMENT_LENGTH_MIN)
return; // no point in reporting this trifle
// trim to max length
if (!User::isInGroup(U_GROUP_MODERATOR))
$this->_post['body'] = mb_substr($this->_post['body'], 0, (CommunityContent::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1)));
$update = array(
'body' => $this->_post['body'],
'editUserId' => User::$id,
'editDate' => time()
);
if (User::$id == $ownerId)
$update['roles'] = User::$groups;
if (User::isInGroup(U_GROUP_MODERATOR))
{
$update['responseBody'] = $this->_post['response'] ?? '';
$update['responseUserId'] = User::$id;
$update['responseRoles'] = User::$groups;
}
DB::Aowow()->query('UPDATE ?_comments SET `editCount` = `editCount` + 1, ?a WHERE `id` = ?d', $update, $this->_get['id']);
}
}
?>

View File

@@ -0,0 +1,45 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// expects non-200 header on error
class CommentFlagreplyResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_VALIDATE_INT]
);
protected function generate() : void
{
if (!$this->assertPOST('id'))
{
trigger_error('CommentFlagreplyResponse - malformed request received', E_USER_ERROR);
$this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : '');
}
$replyOwner = DB::Aowow()->selectCell('SELECT `userId` FROM ?_commments WHERE `id` = ?d', $this->_post['id']);
if (!$replyOwner)
{
trigger_error('CommentFlagreplyResponse - reply not found', E_USER_ERROR);
$this->generate404(Lang::main('intError'));
}
// ui element should not be present
if ($replyOwner == User::$id)
$this->generate404();
$report = new Report(Report::MODE_COMMENT, Report::CO_INAPPROPRIATE, $this->_post['id']);
if (!$report->create('Report Reply Button Click'))
$this->generate404('LANG.ct_resp_error'.$report->getError());
else if (count($report->getSimilar()) >= CommunityContent::REPORT_THRESHOLD_AUTO_DELETE)
DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d WHERE `id` = ?d', CC_FLAG_DELETED, $this->_post['id']);
}
}
?>

View File

@@ -0,0 +1,58 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// toggle flag
class CommentOutofdateResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_VALIDATE_INT ],
'remove' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]],
'reason' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ]
);
protected function generate() : void
{
if (!$this->assertPOST('id'))
{
trigger_error('CommentOutofdateResponse - malformed request received', E_USER_ERROR);
if (User::isInGroup(U_GROUP_STAFF))
$this->result = 'malformed request received';
}
$ok = false;
if (User::isInGroup(U_GROUP_MODERATOR)) // directly mark as outdated
{
if (!$this->_post['remove'])
$ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d WHERE `id` = ?d', CC_FLAG_OUTDATED, $this->_post['id']);
else
$ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` = ?d', CC_FLAG_OUTDATED, $this->_post['id']);
}
else // try to report as outdated
{
$report = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id']);
if (!$report->create($this->_post['reason']))
$this->result = Lang::main('intError');
if (count($report->getSimilar()) >= CommunityContent::REPORT_THRESHOLD_AUTO_OUT_OF_DATE)
$ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d WHERE `id` = ?d', CC_FLAG_OUTDATED, $this->_post['id']);
}
if (!$ok)
{
trigger_error('CommentOutofdateResponse - failed to update comment in db', E_USER_ERROR);
$this->result = Lang::main('intError');
return;
}
$this->result = 'ok'; // the js expects the actual characters 'ok' on success, not some json string like '"ok"'
}
}
?>

View File

@@ -0,0 +1,31 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// up/down - distribution
class CommentRatingResponse extends TextResponse
{
protected array $expectedGET = array(
'id' => ['filter' => FILTER_VALIDATE_INT]
);
protected function generate() : void
{
if (!$this->assertGET('id'))
{
$this->result = Util::toJSON(['success' => 0]);
return;
}
if ($votes = DB::Aowow()->selectRow('SELECT 1 AS "success", SUM(IF(`value` > 0, `value`, 0)) AS "up", SUM(IF(`value` < 0, -`value`, 0)) AS "down" FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND `userId` <> 0 GROUP BY `entry`', RATING_COMMENT, $this->_get['id']))
$this->result = Util::toJSON($votes);
else
$this->result = Util::toJSON(['success' => 1, 'up' => 0, 'down' => 0]);
}
}
?>

View File

@@ -0,0 +1,24 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class CommentShowrepliesResponse extends TextResponse
{
protected array $expectedGET = array(
'id' => ['filter' => FILTER_VALIDATE_INT]
);
protected function generate() : void
{
if (!$this->assertGET('id'))
$this->result = Util::toJSON([]);
else
$this->result = Util::toJSON(CommunityContent::getCommentReplies($this->_get['id']));
}
}
?>

View File

@@ -0,0 +1,34 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// toggle flag
class CommentStickyResponse extends TextResponse
{
protected int $requiredUserGroup = U_GROUP_MODERATOR;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_VALIDATE_INT ],
'sticky' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 1]]
);
protected function generate() : void
{
if (!$this->assertPOST('id', 'sticky'))
{
trigger_error('CommentStickyResponse - malformed request received', E_USER_ERROR);
return;
}
if ($this->_post['sticky'])
DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d WHERE `id` = ?d', CC_FLAG_STICKY, $this->_post['id']);
else
DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` = ?d', CC_FLAG_STICKY, $this->_post['id']);
}
}
?>

View File

@@ -0,0 +1,48 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class CommentUndeleteResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']],
// 'username' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ]
);
protected function generate() : void
{
if (!$this->assertPOST('id'))
{
trigger_error('CommentUndeleteResponse - malformed request received', E_USER_ERROR);
return;
}
// in theory, there is a username passed alongside if executed from userpage... lets just use the current user (see user.js)
$ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` IN (?a) { AND `userId` = `deleteUserId` AND `deleteUserId` = ?d }',
CC_FLAG_DELETED,
$this->_post['id'],
User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id
);
// unflag subject: hasComment
if ($ok)
{
$coInfo = DB::Aowow()->select('SELECT `type` AS "0", `typeId` AS "1" FROM ?_comments WHERE `id` IN (?a) GROUP BY `type`, `typeId`', $this->_post['id']);
foreach ($coInfo as [$type, $typeId])
if ($tbl = Type::getClassAttrib($type, 'dataTable'))
DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_COMMENT, $typeId);
return;
}
trigger_error('CommentUndeleteResponse - user #'.User::$id.' could not unflag comment(s) #'.implode(', ', $this->_post['id']).' from deleted', E_USER_ERROR);
}
}
?>

View File

@@ -0,0 +1,61 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// expects non-200 header on error
class CommentUpvotereplyResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'id' => ['filter' => FILTER_VALIDATE_INT]
);
protected function generate() : void
{
if (!$this->assertPOST('id'))
{
trigger_error('CommentUpvotereplyResponse - malformed request received', E_USER_ERROR);
$this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : '');
}
if (!User::canUpvote())
$this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'cannot upvote' : '');
$comment = DB::Aowow()->selectRow('SELECT `userId`, IF(`flags` & ?d, 1, 0) AS "deleted" FROM ?_comments WHERE `id` = ?d', CC_FLAG_DELETED, $this->_post['id']);
if (!$comment)
{
trigger_error('CommentUpvotereplyResponse - comment #'.$this->_post['id'].' not found in db', E_USER_ERROR);
$this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'replyID not found' : '');
}
if (User::$id == $comment['userId']) // not worth logging?
$this->generate404('LANG.voteself_tip');
if ($comment['deleted'])
$this->generate404('LANG.votedeleted_tip');
$ok = DB::Aowow()->query(
'INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)',
RATING_COMMENT,
$this->_post['id'],
User::$id,
User::canSupervote() ? 2 : 1
);
if (!is_int($ok))
{
trigger_error('CommentUpvotereplyResponse - write to db failed', E_USER_ERROR);
$this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'write to db failed' : '');
}
Util::gainSiteReputation($comment['userId'], SITEREP_ACTION_UPVOTED, ['id' => $this->_post['id'], 'voterId' => User::$id]);
User::decrementDailyVotes();
}
}
?>

View File

@@ -0,0 +1,84 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// up, down and remove
class CommentVoteResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedGET = array(
'id' => ['filter' => FILTER_VALIDATE_INT ],
'rating' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => -2, 'max_range' => 2]]
);
protected function generate(): void
{
if (!$this->assertGET('id', 'rating'))
{
trigger_error('CommentVoteResponse - malformed request received', E_USER_ERROR);
$this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]);
return;
}
if (User::getCurrentDailyVotes() <= 0)
{
$this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('tooManyVotes')]);
return;
}
$target = DB::Aowow()->selectRow(
'SELECT c.`userId` AS "owner", ur.`value`, IF(c.`flags` & ?d, 1, 0) AS "deleted" FROM ?_comments c LEFT JOIN ?_user_ratings ur ON ur.`type` = ?d AND ur.`entry` = c.id AND ur.`userId` = ?d WHERE c.id = ?d',
CC_FLAG_DELETED, RATING_COMMENT, User::$id, $this->_get['id']
);
if (!$target)
{
trigger_error('CommentVoteResponse - target comment #'.$this->_get['id'].' not found', E_USER_ERROR);
$this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]);
return;
}
$val = User::canSupervote() ? 2 : 1;
if ($this->_get['rating'] < 0)
$val *= -1;
if (User::$id == $target['owner'] || $val != $this->_get['rating'] || $target['deleted'])
{
// circumvented the checks in JS
$this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]);
return;
}
if (($val > 0 && !User::canUpvote()) || ($val < 0 && !User::canDownvote()))
{
$this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('bannedRating')]);
return;
}
$ok = false;
// old and new have same sign; undo vote (user may have gained/lost access to superVote in the meantime)
if ($target['value'] && ($target['value'] < 0) == ($val < 0))
$ok = DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND `userId` = ?d', RATING_COMMENT, $this->_get['id'], User::$id);
else // replace, because we may be overwriting an old, opposing vote
if ($ok = DB::Aowow()->query('REPLACE INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)', RATING_COMMENT, $this->_get['id'], User::$id, $val))
User::decrementDailyVotes(); // do not refund retracted votes!
if ($ok)
{
if ($val > 0) // gain rep
Util::gainSiteReputation($target['owner'], SITEREP_ACTION_UPVOTED, ['id' => $this->_get['id'], 'voterId' => User::$id]);
else if ($val < 0)
Util::gainSiteReputation($target['owner'], SITEREP_ACTION_DOWNVOTED, ['id' => $this->_get['id'], 'voterId' => User::$id]);
$this->result = Util::toJSON(['error' => 0]);
}
else
$this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('intError')]);
}
}
?>

View File

@@ -0,0 +1,116 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
// tabId 1: Tools g_initHeader()
class CompareBaseResponse extends TemplateResponse
{
protected string $template = 'compare';
protected string $pageName = 'compare';
protected ?int $activeTab = parent::TAB_TOOLS;
protected array $breadcrumb = [1, 3];
protected array $dataLoader = ['weight-presets', 'gems', 'enchants', 'itemsets'];
protected array $scripts = array(
[SC_JS_FILE, 'js/profile.js'],
[SC_JS_FILE, 'js/Draggable.js'],
[SC_JS_FILE, 'js/filters.js'],
[SC_JS_FILE, 'js/Summary.js'],
[SC_CSS_FILE, 'css/Summary.css']
);
protected array $expectedGET = array(
'compare' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCompareString']]
);
protected array $expectedCOOKIE = array(
'compare_groups' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCompareString']]
);
public Summary $summary;
public array $cmpItems = [];
private string $compareString = '';
public function __construct($pageParam)
{
parent::__construct($pageParam);
// prefer GET over COOKIE
if ($this->_get['compare'])
$this->compareString = $this->_get['compare'];
else if ($this->_cookie['compare_groups'])
$this->compareString = $this->_cookie['compare_groups'];
}
protected function generate() : void
{
$this->h1 = Lang::main('compareTool');
array_unshift($this->title, $this->h1);
$this->summary = new Summary(array(
'template' => 'compare',
'id' => 'compare',
'parent' => 'compare-generic'
));
if ($this->compareString)
{
$items = [];
foreach (explode(';', $this->compareString) as $itemsString)
{
$suGroup = [];
foreach (explode(':', $itemsString) as $itemDef)
{
// [itemId, subItem, permEnch, tempEnch, gem1, gem2, gem3, gem4]
$params = array_pad(array_map('intVal', explode('.', $itemDef)), 8, 0);
$items[] = $params[0];
$suGroup[] = $params;
}
$this->summary->addGroup($suGroup);
}
$iList = new ItemList(array(['i.id', $items]));
$data = $iList->getListviewData(ITEMINFO_SUBITEMS | ITEMINFO_JSON);
foreach ($iList->iterate() as $itemId => $__)
{
if (empty($data[$itemId]))
continue;
if (!empty($data[$itemId]['subitems']))
foreach ($data[$itemId]['subitems'] as &$si)
{
$si['enchantment'] = implode(', ', $si['enchantment']);
unset($si['chance']);
}
$this->cmpItems[$itemId] = [
'name_'.Lang::getLocale()->json() => $iList->getField('name', true),
'quality' => $iList->getField('quality'),
'icon' => $iList->getField('iconString'),
'jsonequip' => $data[$itemId]
];
}
}
parent::generate();
}
protected static function checkCompareString(string $val) : string
{
$val = urldecode($val);
if (preg_match('/[^-?\d\.:;]/', $val))
return '';
return $val;
}
}
?>

View File

@@ -0,0 +1,49 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class ContactusBaseResponse extends TextResponse
{
protected array $expectedPOST = array(
'mode' => ['filter' => FILTER_VALIDATE_INT ],
'reason' => ['filter' => FILTER_VALIDATE_INT ],
'ua' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ],
'appname' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ],
'page' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/']],
'desc' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ],
'id' => ['filter' => FILTER_VALIDATE_INT ],
'relatedurl' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/']],
'email' => ['filter' => FILTER_SANITIZE_EMAIL ]
);
/* responses
0: success
1: captcha invalid
2: description too long
3: reason missing
7: already reported
$: prints response
*/
protected function generate() : void
{
if (!$this->assertPOST('mode', 'reason'))
{
$this->result = 4;
return;
}
$report = new Report($this->_post['mode'], $this->_post['reason'], $this->_post['id']);
if ($report->create($this->_post['desc'], $this->_post['ua'], $this->_post['appname'], $this->_post['page'], $this->_post['relatedurl'], $this->_post['email']))
$this->result = 0;
else if (($e = $report->getError()) > 0)
$this->result = $e;
else
$this->result = Lang::main('intError');
}
}
?>

View File

@@ -0,0 +1,54 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class CookieBaseResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedGET = array(
'purge' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']]
);
public function __construct(private string $param)
{
// note that parent::__construct has to come after this
if ($param && preg_match('/^[\w-]+$/i', $param))
$this->expectedGET = [$param => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']]];
// NOW we know, what to expect and sanitize
parent::__construct($param);
}
/* responses
0: success
$: silent error
*/
protected function generate() : void
{
if (!$this->param && $this->_get['purge'])
{
if (User::$id && DB::Aowow()->query('UPDATE ?_account_cookies SET `data` = "purged" WHERE `userId` = ?d AND `name` LIKE "announcement-%"', User::$id) !== null)
$this->result = 0;
return;
}
if (!$this->param || !$this->assertGET($this->param))
{
trigger_error('CookieBaseResponse - malformed request received', E_USER_ERROR);
return;
}
if (DB::Aowow()->query('REPLACE INTO ?_account_cookies VALUES (?d, ?, ?)', User::$id, $this->param, $this->_get[$this->param]))
$this->result = 0;
else
trigger_error('CookieBaseResponse - write to db failed', E_USER_ERROR);
}
}
?>

View File

@@ -0,0 +1,76 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class CurrenciesBaseResponse extends TemplateResponse implements ICache
{
use TrListPage, TrCache;
protected int $type = Type::CURRENCY;
protected int $cacheType = CACHE_TYPE_LIST_PAGE;
protected string $template = 'list-page-generic';
protected string $pageName = 'currencies';
protected ?int $activeTab = parent::TAB_DATABASE;
protected array $breadcrumb = [0, 15];
protected array $validCats = [1, 2, 3, 22];
public function __construct(string $pageParam)
{
$this->getCategoryFromUrl($pageParam);
parent::__construct($pageParam);
}
protected function generate() : void
{
$this->h1 = Util::ucFirst(Lang::game('currencies'));
/**************/
/* Page Title */
/**************/
array_unshift($this->title, $this->h1);
if ($this->category)
array_unshift($this->title, Lang::currency('cat', $this->category[0]));
/*************/
/* Menu Path */
/*************/
if ($this->category)
$this->breadcrumb[] = $this->category[0];
/****************/
/* Main Content */
/****************/
$this->redButtons[BUTTON_WOWHEAD] = true;
$conditions = [];
if (!User::isInGroup(U_GROUP_EMPLOYEE))
$conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0];
if ($this->category)
$conditions[] = ['category', $this->category[0]];
$money = new CurrencyList($conditions);
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]);
$this->lvTabs->addListviewTab(new Listview(['data' => $money->getListviewData()], CurrencyList::$brickFile));
parent::generate();
}
}
?>

View File

@@ -0,0 +1,267 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class CurrencyBaseResponse extends TemplateResponse implements ICache
{
use TrDetailPage, TrCache;
protected int $cacheType = CACHE_TYPE_DETAIL_PAGE;
protected string $template = 'detail-page-generic';
protected string $pageName = 'currency';
protected ?int $activeTab = parent::TAB_DATABASE;
protected array $breadcrumb = [0, 15];
public int $type = Type::CURRENCY;
public int $typeId = 0;
private CurrencyList $subject;
public function __construct(string $id)
{
parent::__construct($id);
$this->typeId = intVal($id);
$this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE;
}
protected function generate() : void
{
$this->subject = new CurrencyList(array(['id', $this->typeId]));
if ($this->subject->error)
$this->generateNotFound(Lang::game('currency'), Lang::currency('notFound'));
$this->h1 = $this->subject->getField('name', true);
$this->gPageInfo += array(
'type' => $this->type,
'typeId' => $this->typeId,
'name' => $this->h1
);
$_relItemId = $this->subject->getField('itemId');
/**************/
/* Page Title */
/**************/
array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('currency')));
/*************/
/* Menu Path */
/*************/
$this->breadcrumb[] = $this->subject->getField('category');
/***********/
/* Infobox */
/**********/
$infobox = Lang::getInfoBoxForFlags(intval($this->subject->getField('cuFlags')));
// cap
if ($_ = $this->subject->getField('cap'))
$infobox[] = Lang::currency('cap').Lang::nf($_);
// id
$infobox[] = Lang::currency('id') . $this->typeId;
// icon
if ($_ = $this->subject->getField('iconId'))
{
$infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]';
$this->extendGlobalIds(Type::ICON, $_);
}
// original name
if (Lang::getLocale() != Locale::EN)
$infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]';
if ($infobox)
$this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0');
/****************/
/* Main Content */
/****************/
$hi = $this->subject->getJSGlobals()[Type::CURRENCY][$this->typeId]['icon'];
if ($hi[0] == $hi[1])
unset($hi[1]);
$this->headIcons = $hi;
$this->redButtons = array(
BUTTON_WOWHEAD => true,
BUTTON_LINKS => true
);
if ($_ = $this->subject->getField('description', true))
$this->extraText = new Markup($_, ['dbpage' => true, 'allow' => Markup::CLASS_ADMIN], 'text-generic');
/**************/
/* Extra Tabs */
/**************/
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true);
if ($this->typeId != CURRENCY_HONOR_POINTS && $this->typeId != CURRENCY_ARENA_POINTS)
{
// tabs: this currency is contained in..
$lootTabs = new LootByItem($_relItemId);
if ($lootTabs->getByItem())
{
$this->extendGlobalData($lootTabs->jsGlobals);
foreach ($lootTabs->iterate() as [$template, $tabData])
{
if ($template == 'npc' || $template == 'object')
$this->addDataLoader('zones');
if ($template != 'quest')
{
foreach ($tabData['data'] as &$row)
if (!empty($row['stack']))
$row['currency'] = [[$this->typeId, $row['stack'][0]]];
$tabData['extraCols'][] = '$Listview.extraCols.currency';
}
$this->lvTabs->addListviewTab(new Listview($tabData, $template));
}
}
// tab: sold by
$itemObj = new ItemList(array(['id', $_relItemId]));
if (!empty($itemObj->getExtendedCost()[$_relItemId]))
{
$vendors = $itemObj->getExtendedCost()[$_relItemId];
$this->extendGlobalData($itemObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
$soldBy = new CreatureList(array(['id', array_keys($vendors)]));
if (!$soldBy->error)
{
$sbData = $soldBy->getListviewData();
$extraCols = ['$Listview.extraCols.stock', "\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost', '$Listview.extraCols.condition'];
foreach ($sbData as $k => &$row)
{
$items = [];
$tokens = [];
// note: can only display one entry per row, so only use first entry of each vendor
foreach ($vendors[$k][0] as $id => $qty)
{
if (is_string($id))
continue;
if ($id > 0)
$tokens[] = [$id, $qty];
else if ($id < 0)
$items[] = [-$id, $qty];
}
if ($e = $vendors[$k][0]['event'])
if (Conditions::extendListviewRow($row, Conditions::SRC_NONE, $k, [Conditions::ACTIVE_EVENT, $e]))
$this->extendGlobalIds(Type::WORLDEVENT, $e);
$row['stock'] = $vendors[$k][0]['stock'];
$row['stack'] = $itemObj->getField('buyCount');
$row['cost'] = array(
$itemObj->getField('buyPrice'),
$items ?: null,
$tokens ?: null
);
}
// no conditions > remove conditions column
if (!array_column($sbData, 'condition'))
array_pop($extraCols);
$this->addDataLoader('zones');
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $sbData,
'name' => '$LANG.tab_soldby',
'id' => 'sold-by-npc',
'extraCols' => $extraCols,
'hiddenCols' => ['level', 'type']
), CreatureList::$brickFile));
}
}
}
// tab: created by (spell) [for items its handled in LootByItem]
if ($this->typeId == CURRENCY_HONOR_POINTS)
{
$createdBy = new SpellList(array(['effect1Id', SPELL_EFFECT_ADD_HONOR], ['effect2Id', SPELL_EFFECT_ADD_HONOR], ['effect3Id', SPELL_EFFECT_ADD_HONOR], 'OR'));
if (!$createdBy->error)
{
$this->extendGlobalData($createdBy->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
$tabData = array(
'data' => $createdBy->getListviewData(),
'name' => '$LANG.tab_createdby',
'id' => 'created-by',
);
if ($createdBy->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8'))
$tabData['visibleCols'] = ['reagents'];
$this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile));
}
}
// tab: currency for
$n = $w = null;
if ($this->typeId == CURRENCY_ARENA_POINTS)
{
$n = '?items&filter=cr=145;crs=1;crv=0';
$w = '`reqArenaPoints` > 0';
}
else if ($this->typeId == CURRENCY_HONOR_POINTS)
{
$n = '?items&filter=cr=144;crs=1;crv=0';
$w = '`reqHonorPoints` > 0';
}
else
$w = '`reqItemId1` = '.$_relItemId.' OR `reqItemId2` = '.$_relItemId.' OR `reqItemId3` = '.$_relItemId.' OR `reqItemId4` = '.$_relItemId.' OR `reqItemId5` = '.$_relItemId;
if (!$n && !is_null(ItemListFilter::getCriteriaIndex(158, $_relItemId)))
$n = '?items&filter=cr=158;crs='.$_relItemId.';crv=0';
$xCosts = DB::Aowow()->selectCol('SELECT `id` FROM ?_itemextendedcost WHERE '.$w);
$boughtBy = $xCosts ? DB::World()->selectCol('SELECT `item` FROM npc_vendor WHERE `extendedCost` IN (?a) UNION SELECT `item` FROM game_event_npc_vendor WHERE `extendedCost` IN (?a)', $xCosts, $xCosts) : [];
if ($boughtBy)
{
$boughtBy = new ItemList(array(['id', $boughtBy]));
if (!$boughtBy->error)
{
$tabData = array(
'data' => $boughtBy->getListviewData(ITEMINFO_VENDOR, [Type::CURRENCY => $this->typeId]),
'name' => '$LANG.tab_currencyfor',
'id' => 'currency-for',
'extraCols' => ["\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost']
);
if ($n)
$tabData['note'] = sprintf(Util::$filterResultString, $n);
$this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile));
$this->extendGlobalData($boughtBy->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
}
}
parent::generate();
}
}
?>

View File

@@ -0,0 +1,50 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class CurrencyPowerResponse extends TextResponse implements ICache
{
use TrTooltip, TrCache;
private const /* string */ POWER_TEMPLATE = '$WowheadPower.registerCurrency(%d, %d, %s);';
protected int $type = Type::CURRENCY;
protected int $typeId = 0;
protected int $cacheType = CACHE_TYPE_TOOLTIP;
protected array $expectedGET = array(
'domain' => ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']]
);
public function __construct(string $id)
{
parent::__construct($id);
// temp locale
if ($this->_get['domain'])
Lang::load($this->_get['domain']);
$this->typeId = intVal($id);
}
protected function generate() : void
{
$currency = new CurrencyList(array(['id', $this->typeId]));
if ($currency->error)
$this->cacheType = CACHE_TYPE_NONE;
else
$opts = array(
'name' => $currency->getField('name', true),
'tooltip' => $currency->renderTooltip(),
'icon' => $currency->getField('iconString')
);
$this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []);
}
}
?>

149
endpoints/data/data.php Normal file
View File

@@ -0,0 +1,149 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class DataBaseResponse extends TextResponse
{
protected array $expectedGET = array(
'locale' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkLocale' ]],
't' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine' ]],
'catg' => ['filter' => FILTER_VALIDATE_INT ],
'skill' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkSkill' ]],
'class' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 11]],
'callback' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCallback' ]]
);
public function __construct(string $pageParam)
{
parent::__construct($pageParam);
if ($this->_get['locale']?->validate())
Lang::load($this->_get['locale']);
}
protected function generate() : void
{
// different data can be strung together
foreach ($this->params as $set)
{
// requires valid token to hinder automated access
if ($set != 'item-scaling' && (!$this->_get['t'] || empty($_SESSION['dataKey']) || $this->_get['t'] != $_SESSION['dataKey']))
{
trigger_error('DataBaseResponse::generate - session data key empty or expired', E_USER_ERROR);
continue;
}
/* issue on no initial data:
when we loadOnDemand, the jScript tries to generate the catg-tree before it is initialized
it cant be initialized, without loading the data as empty catg are omitted
loading the data triggers the generation of the catg-tree
*/
$this->result .= match($set)
{
'factions' => $this->loadProfilerData($set),
'mounts' => $this->loadProfilerData($set, SKILL_MOUNTS),
'companions' => $this->loadProfilerData($set, SKILL_COMPANIONS),
'quests' => $this->loadProfilerQuests($set, $this->_get['catg']),
'recipes' => $this->loadProfilerRecipes(),
// locale independent
'quick-excludes',
'weight-presets',
'item-scaling',
'realms',
'statistics' => $this->loadAgnosticFile($set),
// localized
'talents',
'achievements',
'pet-talents',
'glyphs',
'gems',
'enchants',
'itemsets',
'pets',
'zones' => $this->loadLocalizedFile($set),
default => (function($x) { trigger_error('DataBaseResponse::generate - invalid file "'.$x.'" in request', E_USER_ERROR); })($set),
};
}
}
private function loadProfilerRecipes() : string
{
if (!$this->_get['callback'] || !$this->_get['skill'])
return '';
$result = '';
foreach ($this->_get['skill'] as $s)
Util::loadStaticFile('p-recipes-'.$s, $result, true);
Util::loadStaticFile('p-recipes-sec', $result, true);
$result .= "\n\$WowheadProfiler.loadOnDemand('recipes', null);\n";
return $result;
}
private function loadProfilerQuests(string $file, ?string $catg = null) : string
{
$result = '';
if ($catg === null)
Util::loadStaticFile('p-'.$file, $result, false);
else
Util::loadStaticFile('p-'.$file.'-'.$catg, $result, true);
$result .= "\n\$WowheadProfiler.loadOnDemand('".$file."', ".($catg ?? 'null').");\n";
return $result;
}
private function loadProfilerData(string $file, ?string $catg = null) : string
{
$result = '';
if ($this->_get['callback'])
if (Util::loadStaticFile('p-'.$file, $result, true))
$result .= "\n\$WowheadProfiler.loadOnDemand('".$file."', ".($catg ?? 'null').");\n";
return $result;
}
private function loadAgnosticFile(string $file) : string
{
$result = '';
if (!Util::loadStaticFile($file, $result) && Cfg::get('DEBUG'))
$result .= "alert('could not fetch static data: ".$file."');";
return $result . "\n\n";
}
private function loadLocalizedFile(string $file) : string
{
$result = '';
if ($file == 'talents' && ($_ = $this->_get['class']))
$file .= "-".$_;
if (!Util::loadStaticFile($file, $result, true) && Cfg::get('DEBUG'))
$result .= "alert('could not fetch static data: ".$file." for locale: ".Lang::getLocale()->json()."');";
return $result . "\n\n";
}
protected static function checkSkill(string $val) : array
{
return array_intersect(array_merge(SKILLS_TRADE_PRIMARY, [SKILL_FIRST_AID, SKILL_COOKING, SKILL_FISHING]), explode(',', $val));
}
protected static function checkCallback(string $val) : bool
{
return substr($val, 0, 29) === '$WowheadProfiler.loadOnDemand';
}
}
?>

48
endpoints/edit/image.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class EditImageResponse extends TextResponse
{
protected bool $requiresLogin = true;
protected array $expectedGET = array(
'qqfile' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ],
'guide' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]]
);
/*
success: bool
id: image enumerator
type: 3 ? png : jpg
name: old filename
error: errString
*/
protected function generate() : void
{
if (!$this->assertGET('qqfile', 'guide'))
{
$this->result = Util::toJSON(['success' => false, 'error' => Lang::main('genericError')]);
return;
}
if (!User::canWriteGuide())
{
$this->result = Util::toJSON(['success' => false, 'error' => Lang::main('genericError')]);
return;
}
$this->result = GuideMgr::handleUpload();
if (isset($this->result['success']))
$this->result += ['name' => $this->_get['qqfile']];
$this->result = Util::toJSON($this->result);
}
}
?>

Some files were not shown because too many files have changed in this diff Show More