835 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
b890d6504e Guides: initial implementation
* a guide is a wrapper around an article providing management tools.
 * administration is limited to the review process. Needs to be expanded.
 * articles on DB pages are seperate. Editor will be added in the future.
2022-03-22 15:43:39 +01:00
Sarjuuk
33a870ef78 Structure/Types
* move distributed constant values to object
 * move reused iterators and checks to object
2022-03-22 15:05:50 +01:00
Sarjuuk
e109a6deed Siteconfig/Email
* make hardcoded email configurable
2022-03-22 14:59:45 +01:00
Sarjuuk
feb6ee25dd Misc/Fixups
* lost changes
2022-03-22 14:59:13 +01:00
Sarjuuk
a8edf6c912 POST/GET
* unify accessing &_GET and &_POST data
 * properly calc and display diffTime
2022-03-22 14:55:43 +01:00
Sarjuuk
3cb02f2204 JS/CSS
* unify handling
2022-03-22 14:45:55 +01:00
Sarjuuk
4b32811424 Tooltips/Errors
* do not send 404 header for xml or tooltips
2022-03-18 17:28:39 +01:00
Sarjuuk
4972cc0faf POST/GET
* unify accessing &_GET and &_POST data
2022-03-17 13:47:48 +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
e092a69175 JS/Misc
* grid tables no longer clear floating elements (have an eye out if this broke something)
 * add 1px to inner tooltips so it doesn't unexpectedly line break
2022-03-11 15:13:29 +01:00
Sarjuuk
1c7acf6a08 CommunityContent/Misc
* granularly define inclusion of comments, screenshots or videos
 * i hope everyone is on a 2y old php version by now
2022-03-10 14:09:38 +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
Sarjuuk
012ebe578b Items/Filter
* apply stats and name suffix to result set for filter #124 - random enchantment
 * fix filter #125 - required arena team rating
 * fix filter #85 - objective of quest
2022-03-06 09:52:19 +01:00
Sarjuuk
e125cab690 Search/Spells
* do not search for npc abilities under misc. spells tab
2022-03-04 10:27:19 +01:00
Sarjuuk
a1e7bfaa02 Quest/Detailpage
* fixed breadcrumb trail for quests categorized to subzones
 * display auto accept status in infobox
2022-03-04 10:01:30 +01:00
Sarjuuk
73c1118601 Setup/Update
* reapply flags for community content when rebuilding DB
   fixes associated filters and meta page 'missing screenshots'
2022-03-03 19:07:10 +01:00
Sarjuuk
33ec14f232 Merge remote-tracking branch 'github/master' into ghMaster 2022-02-28 21:03:36 +01:00
Sarjuuk
366f68f24f Setup/Misc
* squash some more php warnings during setup
 * automaticly sync if required by updates
2022-02-28 21:01:07 +01:00
Sarjuuk
91f8fe8925 Setup/Mails
* add lost world db dependencies to script
2022-02-28 12:52:09 +01:00
Sarjuuk
8431329645 SmartAI/Spells
* fixed fetching casters for spellId (as seen on 'used by' tabs)
2022-02-28 09:11:22 +01:00
Sarjuuk
19584feab0 Setup/Itemset
* fixed missing items in set
 * fixed php warnings during setup
 * fixed triggering scripts when changing config values
2022-02-27 23:53:36 +01:00
Sarjuuk
bc71ae762c Setup/Data
* move custom data from setup scripts to DB
 * move strings from DB to locale files
 * use common function to calculate coordinates of dungeon entrances instead of inline sql
 * fixed manual data zones and itemset
 * fixed typos across the board
2022-02-21 23:31:14 +01:00
Sarjuuk
10805a1f70 Profiler/Regions
* add Asia regions
 * fixed some typos
2022-02-20 22:47:54 +01:00
Sarjuuk
a24e8594fb Profiler/Localization
* support non-latin realm names for guilds and arena teams also
2022-02-16 04:17:40 +01:00
Sarjuuk
01a9744ba7 Locks
* reworked how an dwhen locks on GameObjects and Items are displayed
 * added structure for LockType.dbc
2022-02-14 23:17:51 +01:00
Sarjuuk
53e2af2116 Profiler/Localization
* support non-latin realm names
 * actually set characters as updated after resync was skipped due to character not having logged in since last resync
2022-02-14 16:04:57 +01:00
Metalaka
4255933328 Creature/Loot
* display skill condition on drop (#325)
2022-02-13 23:44:36 +01:00
Sarjuuk
1856f0ef8f README/QA
* added another repeat gotcha to the Q+A section
2022-02-13 23:43:12 +01:00
Sarjuuk
731f648cd2 Setup/Misc
* wrap db test in try/catch block
 * escape more mysql8 keywords
2022-02-12 15:22:52 +01:00
Sarjuuk
6a2511866e Conditions/Localization
* fixed index for descriptions
 thx @Faq for noticing
2022-02-12 14:37:32 +01:00
Sarjuuk
b777262670 Merge branch 'master' of github.com:Sarjuuk/aowow 2022-02-12 14:27:10 +01:00
Sarjuuk
c950cdc757 Setup/Misc
* fixed variable name: setup parameter --locales should work again.
 * escaped GROUPS as keyword in mysql8
2022-02-12 14:21:07 +01:00
Sarjuuk
541224f87b Markup/Misc
* fixed ajax fulltext sanitizer eating \s instead of \n after 57864d2544
 * happy new year
2022-01-06 14:21:51 +01:00
Sarjuuk
dcb8995b1a Markup
* allow 'auto' as width for tables
 * db tag 'money' may include currency AND items
2022-01-05 23:32:06 +01:00
Sarjuuk
2ec4809c7f Spells/Reagents
* try to fix wrong reagent amount in cases where one reagent requires multiples of itself and is triggered by an item
2022-01-05 23:27:44 +01:00
Sarjuuk
061449686d Revert "[WIP] Conditions"
This reverts commit a1f0f3d30d.
2021-11-21 09:50:59 +01:00
Sarjuuk
f3f8dacef9 Profiler/Talents (#315)
* fix usage of active talent spec
2021-11-19 20:30:16 +01:00
Sarjuuk
a1f0f3d30d [WIP] Conditions
* add missing conditionsTypes

todo:
 process params in Util::getServerConditions()
 add locale strings to js
 extend verbose display in global.js ConditionList
2021-11-04 22:49:12 +01:00
Sarjuuk
e4b8a8e908 Zone/Mapper
* show fishing nodes on map
2021-11-04 22:40:07 +01:00
Sarjuuk
41f415095a JS/Tooltips
* define tooltip_noequipcooldown
2021-11-04 22:39:58 +01:00
Sarjuuk
b824bc54c2 ItemList/ExtendedCost
* restore internal list pointer after using iterator get extended costs
2021-11-04 22:39:07 +01:00
Sarjuuk
35aca18346 JS/Markup
* enable "event" and "currency" codes for regular users
2021-11-04 22:38:55 +01:00
Sarjuuk
4b37a4fd70 CreatureText/Formating
* removed excess escape on talker name
2021-11-04 22:38:43 +01:00
Sarjuuk
c665062faa Staff/vehicles
* fixed error when trying to lookup accessory creatures in world db with aowow guids
2021-11-04 22:38:30 +01:00
Sarjuuk
fca627c408 * fixed bitshift by negative number issue when evaluating spell MiscValues
* defined upper limit for localeIds and also discard negative localeIds
* fixed eval error when parsing spell descriptions. Calculated numbers sadly can't be localized.
2021-11-04 22:37:15 +01:00
Sarjuuk
ea25776225 Zone/Map
* display mailboxes on zone map
2021-11-04 22:36:26 +01:00
Giacomo Pozzoni
5781dfb69e Fix syntax error (#312)
Fix wrong bracket order revealed by PHP 8
2021-10-27 19:52:15 +02:00
Sarjuuk
60e3f62504 Update README.md
Added two more 'gotchas' to the README.md
2021-10-26 18:53:53 +02:00
Sarjuuk
9cda84ef88 added but report template 2021-10-26 10:57:57 +02:00
Sarjuuk
3495a7a40d Achievememnt/Series
* keep series zero indexed like quests
2021-10-24 21:29:33 +02:00
Sarjuuk
8acc93c325 Search/Tabs
* switch between single and multi-class display as necessary
2021-10-24 21:20:46 +02:00
Sarjuuk
4755058fd0 Merge branch 'master' of https://github.com/Sarjuuk/aowow 2021-10-24 20:59:47 +02:00
Sarjuuk
306f8eb9b6 Locale/enUS
* addition to 5fe9c23be2
2021-10-24 20:59:25 +02:00
wodim
5fe9c23be2 Fix category name in enUS (#239) 2021-10-24 20:58:38 +02:00
Sarjuuk
68e262d96c Locale/zhCN
* analogue to 8727d4f66f
2021-10-24 20:55:33 +02:00
Dima
8727d4f66f Update locale_ruru.js (#291)
missing space
2021-10-24 20:55:02 +02:00
Sarjuuk
07b9ed4275 Setup/CLI
* also fix empty --build param building all files
 * fixed copy/paste typo
2021-10-24 20:41:44 +02:00
Giacomo Pozzoni
cb2274bbab Fix php aowow --sql not doing anything (#307) 2021-10-24 20:40:50 +02:00
Stefano Borzì
7c293c405f remove unused static method getName() in SoundList, close #293 (#294) 2021-10-24 20:30:14 +02:00
Sarjuuk
6819c1b1f2 Misc
* fixed misc php8 errors that went unnoticed as php7 warnings before
 * added lost areatrigger generator to Setup script
 * updated readme with working TCDB info and php 8.0 support
2021-10-24 19:47:45 +02:00
Sarjuuk
57864d2544 PHP8/AjaxHandler
* filter callback function must be defined static
 * fixed some typos
 * fixed commentbody filter callback eating \n chars
2021-10-24 19:23:53 +02:00
Sarjuuk
f9ed75d5af SmartAI/Spells
* fixed fetching spells from SmartAI for Abilities tab

Co-authored-by: jackpoz <giacomopoz@gmail.com>
2021-10-24 18:12:53 +02:00
jackpoz
453690c575 Fix error on windows when no parameters are passed in query string 2021-10-24 17:35:24 +02:00
Sarjuuk
557638b3fe PHP8/Misc
* reduce redundancy after merge
2021-10-24 17:33:47 +02:00
jackpoz
f9584f09b5 Handle is_callable returning false with instance methods on static types since PHP 8
See https://php.watch/versions/8.0/non-static-static-call-fatal-error
2021-10-24 17:15:26 +02:00
Sarjuuk
6b49aa6069 Setup/Misc
* fix some outdated array indizes during setup
 * fix indexing when connecting to DB for first time
 * add initial loading of config strings during setup
2021-10-24 17:12:05 +02:00
Sarjuuk
56e70e22bb Item/DetailPage
* fixed infobox cost for items with buycount > 1
2021-03-14 16:31:35 +01:00
Sarjuuk
6f69144498 Merge remote-tracking branch 'github/master' into ghMaster 2021-03-14 16:13:39 +01:00
Sarjuuk
aa82655845 SmarAI/Lookups
* unify lookups in functional groups
 * move code to class SmartAI
2021-03-14 02:04:56 +01:00
Sarjuuk
c456ade870 Skills/Breakpoints
* display skill learning breakpoints for gathering skills
 * fixed mixed up mining-skinning and savanging-skinning filters for NPCs
 * defined some magic skill numbers
2021-03-14 02:04:43 +01:00
Dima
aa70ea0323 Localization (#284)
use multibyte function to manipulate localized text.
2021-03-02 20:24:11 +01:00
Sarjuuk
fbc5d43aab Setup/Maintenance
* toggle maintenance mode in a more reasonable manner (i.e. not when displaying help test)
 * move connectivity tests to class DB
 * restore generate everything functionality when running --sql and --build with an empty parameter set
2021-02-20 20:35:53 +01:00
Sarjuuk
43778b01e7 Setup/DB
* connect to db after successful setup so siteconfig doesn't fail
2021-02-18 22:31:30 +01:00
Sarjuuk
f5701e7979 Misc/Config
* add config values lost from dd63a6a2ab
 * update TCDB compatibility info
 * update current year
2021-02-18 19:00:55 +01:00
Dima
33c3132346 Localization/ruRU (#281)
* add reputation strings
2021-02-18 13:36:29 +01:00
Sarjuuk
d6ca3d70cf Listview/Percent
* use same logic for display and sorting
2021-02-15 20:07:49 +01:00
Giacomo Pozzoni
392b5bbdda CLI/Typos (#272)
* fixed typos in log
2021-02-15 19:48:35 +01:00
Sarjuuk
e810554695 Mapper/Instances
* do not display unmapped instances (3.3.5 from data) by default
2021-02-15 19:30:48 +01:00
Sarjuuk
8313bb4194 Icons/DetailPage
* don't cast name to ucFirst
2021-02-15 19:16:10 +01:00
Sarjuuk
0c1b73d6ac Icons/Names
* icon names can contain spaces. So indiscriminate trimming should be discouraged.
 * ...
 * unrelated stuff: please don't break...
2021-02-15 19:08:56 +01:00
Sarjuuk
7a74c36448 Setup/Profiler exclusions
* set time limit to generation of profiler exclusions file
 * remove some unused logging
2021-02-15 18:20:01 +01:00
Sarjuuk
23abce304c Readme/Setup
* use more intuitive setup command. "firstrun" is still available but usage is discouraged.
2021-02-15 17:59:36 +01:00
Sarjuuk
e5e62e2936 Merge remote-tracking branch 'github/master' into ghMaster 2021-02-15 17:55:24 +01:00
Dima
c0a1e393b0 Localization/ruRU (#278)
* fixed typo in user contribution template
2021-02-15 17:32:46 +01:00
Dima
898a4fd8a4 Localization/Typo (#279)
* fixed icons path in ruRU and frFR menus
2021-02-15 17:13:10 +01:00
Dima
3978169bf6 Localization/ruRU (#280)
* added missing zone and gameobject strings
2021-02-15 17:10:22 +01:00
Dima
01e89db491 Setup/Help (#276)
* fixed log command instructions
2021-02-15 16:50:25 +01:00
Sarjuuk
59f58f8506 Setup
* automatically set and unset maintenance mode when data is being edited (either sql or files)
 * added more verbose help prompts. They should now always appear when -h is used.
 * added --setup as more intuitive command for --firstrun. --firstrun is now considered deprecated and will be removed in due time.
 * unfuck order of indizes in --siteconfig
 * fixed some typos
 * convert echo use to CLI::write
 * move scattered command line argument checks to CLISetup
2020-12-30 00:29:30 +01:00
Sarjuuk
c65bd88867 Setup/RandPropPoints
* refer to correct dbc file to get item level scaling for random enchantments
2020-12-29 13:20:42 +01:00
Sarjuuk
2eaed47ba4 Items/Misc
* fixed queries erronously using ucFirst column names
 * fixed visible html in infobox
 * added forgotten string for locale 3
2020-12-28 23:27:49 +01:00
Sarjuuk
888ff28121 Item/DetailPage
* alwaya display item level of gems regardless if cut or not
 * fixed similar item search result for items without class restrictions
2020-12-27 20:45:44 +01:00
Sarjuuk
baf47433dd Enchantment/DetailPage
* removed tooltip-highlight when there is no tooltip to display
Spells/Tooltips
 * fixed ancient copy-paste error and 'greater than or equal' conditions are now always evaluated as such
2020-12-27 20:07:57 +01:00
Sarjuuk
89275a0e71 Readme
* remove reference to expired branch
2020-12-19 01:06:30 +01:00
Sarjuuk
15fb7b8711 NPCs/DetailPage
* display difficulty versions of the creature in the infobox
2020-12-19 00:18:46 +01:00
Sarjuuk
467a31fa3b Template/Escaped Strings
* escape creature subnames in DetailPage
 * escape creature names & subnames in Tooltips
 * js escape inherited filter froms
2020-12-19 00:16:09 +01:00
Sarjuuk
5b414500a7 Creature/DetailPage
* display school resistance under stats in infobox
2020-12-19 00:00:49 +01:00
Sarjuuk
cac57f5cd8 Spell/DetailPage
* fixed recursing SmartAI ActionLists for used-by relations
2020-12-19 00:00:49 +01:00
Sarjuuk
3188a7b55f Template/generic detail page
* switch position of article and extra text section
 * pray the layout doesn't break somewhere unexpected
2020-12-19 00:00:49 +01:00
Sarjuuk
4ed2d0836f Localization/Invisibility
* set placeholders for unknown but used invisibilityTypes to suppress errors on access
2020-12-17 22:25:25 +01:00
Sarjuuk
be77474637 Misc/Optimizations
* general minor cleanups
 * do not sync characters that haven't logged in since last resync.
2020-12-17 22:24:42 +01:00
Sarjuuk
82d3a8508d Localization/Placeholders
* apply changes from d070b303b4 to placeholder texts
2020-10-25 17:56:12 +01:00
wodim
d070b303b4 Localization (#237)
* removed excess hyphens
2020-10-25 17:49:26 +01:00
wodim
7062ff74a4 Localization/Typo (#226)
* Fix typo
2020-10-25 17:41:53 +01:00
Sarjuuk
df1f29c986 LinkedSpells/Localization
* Smoothed out word choice. Should be more clear now.
2020-10-25 17:27:24 +01:00
Sarjuuk
178c13ec72 SmartAI/SpellCasts
* fixed getting spell casts per NPC
 * fixed spell usage for NPCs when used by Timed Actionlists
 * added 'used by (smart) areatrigger' to SpellDetailPage
 * fixed forgotten rename from SmartAI overhaul
2020-10-25 17:09:05 +01:00
Sarjuuk
e2fe765980 SAI/Compat
* add changes from TC of the last three years
* reduce redundancy when handling creature text
* reserve more space in target, event, action parameters. Move own parameters further back to avoid confict with future param expansion by TC
2020-10-25 15:47:23 +01:00
Sarjuuk
003ac1c931 Listviews/Quests
* fixed displaying as many quest requirement columns as items requiring quests
2020-10-10 11:08:04 +02:00
Sarjuuk
0430d75989 Tooltips/Cleanup
* removed double escaped json
 * fixed itemset tooltips with classes
 * set cookies to samesite to avoid being rejected by the browser
2020-10-10 11:00:59 +02:00
Sarjuuk
59fb6c1bc9 DetailPage/GameObject
* display related faction in infobox
2020-10-10 10:14:13 +02:00
Sarjuuk
12c5f6f468 Setup/Maps
* fixed small map height (small maps used in tooltips)
 * map files need to be regenerated manually

thx @wodim
2020-09-16 21:29:04 +02:00
Sarjuuk
c92ab37de1 Misc/Typo
* fixed typo in SAI localization

Co-authored-by: wodim <neikokz@gmail.com>
2020-09-16 20:08:13 +02:00
Sarjuuk
88417cd5a6 Setup/Spawns
* added required areatable.dbc to spawns setup script, forgotten in 62acd541b2
2020-09-11 23:08:03 +02:00
Sarjuuk
583f8658d7 Pages/Caching
* use native functions of ReflectionProperty instead of magic numbers. GetModifiers() may return different values then expected/documented
2020-09-02 21:17:05 +02:00
Sarjuuk
04a14393e5 DB/MySQL
* fixed some more use cases of 'rank'
2020-09-02 21:12:00 +02:00
Giacomo Pozzoni
f34b4827ee DB/MySQL (#219)
* fixed usage of field name 'rank' which became a keyword in MySQL 8
2020-09-02 20:58:38 +02:00
Sarjuuk
aac9aab3c9 Spells/DetailPage
* diplay filtrable spell attributes in overview table
 * replace some magic numbers with constants
2020-08-31 18:21:53 +02:00
Sarjuuk
349c32d0a6 SAI/Targets
* add forgotten change from 243429bf68 to target tooltips
2020-08-30 18:28:28 +02:00
Sarjuuk
5179e6e09b SAI/Typo
* fixed typo in SmartAI event handler 76 bricking related object/npc pages
2020-08-30 18:04:57 +02:00
Sarjuuk
366a0f2040 Game/worldPosToZonePos
* catch DB errors resulting in wrong return type
2020-08-30 17:33:27 +02:00
Sarjuuk
93948a2ec5 Spells/DetailPage
* resolve spell effect #86 with info from 203573db83
2020-08-06 16:24:35 +02:00
Sarjuuk
3e24814518 README/Conf
* check for presence of lib GMP when trying to enable TC auth
 * also added missing requirement to README (thx @jackpoz for reminding me)
2020-08-03 22:11:05 +02:00
Sarjuuk
726fe90bb0 README
* hear ye hear ye! the regulations have changed!
 * while the master branch will now use SRP6 for TrinityCores auth, the branch sha1-auth will be available for about three months to ease migration.
 * until Nov. 2020 updates will be applied to both branches
2020-08-03 15:36:49 +02:00
Treeston
fb2ab8f613 SRP6 for user auth (#38)
* SRP6 for user auth, implementation of 3164b58c7d
2020-08-03 15:20:34 +02:00
Sarjuuk
72e950713a Achievement/Criteria
* use reference achievement for criteria if set
2020-07-25 21:53:23 +02:00
Sarjuuk
ce90229a8b Quest/Rewards
* implement changes to max level xp conversion from 670085d8c0
2020-07-25 21:04:36 +02:00
Sarjuuk
4bd32207db Setup/Updates
* skip unexpected characters in update fields of aowow_dbversion
2020-07-20 21:14:06 +02:00
Sarjuuk
844a4c0e52 Setup/Fixups
* added lost changes from
   321f28d35c and
   62acd541b2 to setup
2020-06-25 19:27:33 +02:00
Sarjuuk
d858c85465 Items/Tooltips
* display ppm in tooltip
2020-06-07 21:53:03 +02:00
Sarjuuk
82d3441b22 Spells/DetailPage
* expand used by - tabs to show spells from timed action lists and smart_script casts from gameobjects in general
2020-06-07 21:52:55 +02:00
Sarjuuk
c08773eb6f Screenshots/Misc
* no functional changes
2020-06-06 17:30:04 +02:00
Sarjuuk
240024fb10 Summary/Itemsets
* fixed set bonus calculation when using multiple sets in one summary

Localization
 * create more concise error messages when accessing nonexistant locale strings
2020-06-06 15:55:54 +02:00
Sarjuuk
321f28d35c Achievements/Category
* moved names from db to locale files
2020-06-04 00:56:59 +02:00
Amandil
8e14e4a9f3 fix signin url for locale_frfr (#208) 2020-06-03 20:32:19 +02:00
Sarjuuk
184e5e5c8d Setup/DB
* fixed input prompt broken after iputting aowow's info
2020-06-01 12:24:54 +02:00
Sarjuuk
7b9b542e65 Merge remote-tracking branch 'github/master' into ghMaster 2020-05-30 11:50:21 +02:00
Stefano Borzì
a3d35a7ad5 Update credits (#205)
* Update credits
2020-05-29 18:53:22 +02:00
Sarjuuk
366bdf54a6 User/Cookies
* additional fixes against announcements not staying closed
2020-05-28 19:37:54 +02:00
Sarjuuk
d22e90ca85 DB/Logs
* set up some query profiling
2020-05-28 18:43:24 +02:00
Sarjuuk
c290f845d6 Quest/DetailPage
* added tab and note with quests from the same pool if available

 * fixed announcements poping up after being dismissed previously
 * fixed double escaping of quest tooltips
2020-05-28 18:43:14 +02:00
Sarjuuk
8741c7479f Misc/Fixups
* vehicle accessories now get moved with their vehicle
 * try to account for multilevel zone offset nonsense
 * fixed some more typos and unaccounted null cases
2020-05-27 15:27:13 +02:00
Sarjuuk
62acd541b2 Maps/Spawns
* Entities (Objects, NPCs, ect) can now easily be assigned to a different map to be displayed on by clicking their pip on the map
 * Entities with already assigned area (by TrinityCore) that were unable to be matched onto the map are no longer discarded. They'll now show up in appropriate listviews.
 * Entities without already assigned area that are also unable to be matched onto the map now get an area assigned as long as the relationship areaId <=> mapId is unique (read instanced areas)
2020-05-26 21:11:15 +02:00
Sarjuuk
7db841b823 Misc/Typo
* fixed typo from 6cabfd3
2020-05-25 22:26:45 +02:00
Sarjuuk
6cabfd3864 Misc/Cleanup
* moving commonly used strings to defines
 * moving commonly reused/similar page generation functions to the parent
 * generally using consistent return types, more type hints and less strings
 * prevent browser context menu when right clicking on UI elements with their own context menus
 * fixed menu path for icons
2020-05-24 15:04:42 +02:00
Sarjuuk
b044488308 Profiler/Recipes
* _really_ fix learning starter recipes per skill attempted in 96bbe326a8
2020-05-18 20:23:23 +02:00
Sarjuuk
a1d3be86f7 Profiler/Talents
* fixed column reference changed in f8230a59a9
2020-05-09 16:57:45 +02:00
Sarjuuk
8378354f8b Creatures/Objects
* added some missing links between boss npcs and loot chests
 * use name of group encounters from dungeonencounter.dbc instead of single creature
Profler
 * remove link to blizzard arsenal
 * truncate long titles in raid progression tracker
2020-05-03 16:52:00 +02:00
Sarjuuk
08f8297629 Objects/SpellFocus
* added tab to DetailPage with spells requiring this object to be cast
2020-04-26 14:22:11 +02:00
Sarjuuk
9b4c0bb09e Zones/Misc
* hide some more unused zone
 * link zones Plaguelands: Scarlet Enclave and Onyxia's Lair to correct map
 * fixed wotlk raids all being heroic
 * link icecrown citadel sub maps to master and hide sub maps
2020-04-19 18:42:10 +02:00
Sarjuuk
92a1016a0f Spells/Misc
* display dbc definitions before articles on detail pages
 * fixed logic when displaying skill step numbers
 * made custom spell columns for linked spells and stack rules localizable
2020-04-19 16:51:34 +02:00
Sarjuuk
08c98bc8ee Search/Spells
* fixed some spells being skipped in search (hopefully)
2020-04-10 23:01:15 +02:00
Sarjuuk
b7bf58a664 Profiler/Excludes
* exclude some more unobtainable receipes
2020-04-10 12:35:53 +02:00
Sarjuuk
64ce4826c6 Profiler/Excludes
* tag some more receipes as unavailable
2020-04-05 15:14:49 +02:00
Sarjuuk
96bbe326a8 Profiler/Spells
* fixed applying auto-learned profession spells
2020-04-05 14:24:08 +02:00
Sarjuuk
0f9a3e8cb6 Profiler/Custom
* fixed deleted profiles counting towards the user cap of 10
2020-04-05 12:31:59 +02:00
Sarjuuk
9f8c643cf4 Filter/Criteria
fixed
 * Quest: class/race-specific
 * NPC: skinnable
2020-04-05 11:58:36 +02:00
Sarjuuk
c2a1556e8a Loot/Refloot
* fixed Reference Loot's drop chance counting towards the adaptive drop chance of loot within the same loot group.
2020-04-04 15:20:51 +02:00
Sarjuuk
15246ea964 Util/Time
* fixed time formatting for milliseconds
 * only process absolute time values

Co-authored-by: Helias <stefanoborzi32@gmail.com>
2020-04-01 22:29:36 +02:00
Sarjuuk
3b749025de Items/Itemsets
* fixed erronous linking to the baseline set for virtual sets
 * display items from the same set, for the same slot in the 'see also' tab
2020-03-28 15:56:24 +01:00
Sarjuuk
bf2e1bd612 Profiler/DB
* implement changes from https://github.com/TrinityCore/TrinityCore/commit/1925778e44a
 * thx @kep123 for pointing these out
2020-02-28 22:04:33 +01:00
Sarjuuk
301c29e944 Localization/Profiler
* added some more strings for loc 4 (zhCN)
 * thx @kep123 for providing those
2020-02-28 22:04:03 +01:00
Sarjuuk
54b20c3131 NPC/Spells
* always treat spells from creature_template_spell as controlled abilities
2020-02-26 17:29:33 +01:00
Sarjuuk
55d19589b8 Merge remote-tracking branch 'github/master' into ghMaster 2020-02-26 16:37:48 +01:00
Sarjuuk
59ef124a26 SmartAI
* fixed several ocurences of time strings being cast to int
 * always use short time format
 * also fixed that one questionmark typo that kicked this whole commit off
2020-02-26 16:33:31 +01:00
Sarjuuk
47458e3ec2 Spell/Effects
* also link back from creature to summon spell for spell effects 56 & 112
2020-02-24 23:33:01 +01:00
Giacomo Pozzoni
4d08fb974d Escape MySQL 8 keywords (#193) 2020-02-24 22:02:49 +01:00
Sarjuuk
3f1e44d3e2 Titles/DetailPage
* fix page titles for .. titles
 * escape htmlesque tags not in the template but individually as needed
2020-02-24 21:40:50 +01:00
Sarjuuk
aa66a7644b Areatrigger/Names
* fix unnamed areatrigger after fetching them from DB and not clientside
Mail/Names
 * fix unnamed mails after fetching them from DB and not clientside
2020-02-24 02:00:37 +01:00
Sarjuuk
3ff855afe8 Setup/Mails
* index can actually be negative
2020-02-24 01:36:42 +01:00
Sarjuuk
3c088cd4e8 Spell/Effects
* parse npcs for spell effect 56 and 112
2020-02-23 22:17:16 +01:00
Sarjuuk
0694367ee9 Spell/Effects
* parse triggered spell from effect 140 and 141
 * generally display non-zero effect values
2020-02-23 21:36:26 +01:00
Sarjuuk
4bda806b9b DB/Mails
* add lost column `attachment`
2020-02-23 20:32:40 +01:00
Sarjuuk
b37efc480a Item/Icons
* handle icons containing whitespaces
2020-02-23 19:46:08 +01:00
Sarjuuk
717e9c43be Objects/Names
* fix unnamed objects after fetching them from DB and not clientside
2020-02-23 19:13:43 +01:00
jackpoz
1f84cae1dd Sort zones in ?npc=... by spawn count 2020-02-23 19:01:06 +01:00
Sarjuuk
5bb15d47d7 Misc/Readme
* update required TDB-version (hint: its never 'latest')
 * fixed artifacts from setup conversion
 * version push
2020-02-23 18:45:44 +01:00
Sarjuuk
16bd59e0cc NPC/Trainer
* add support for https://github.com/TrinityCore/TrinityCore/commit/bf3ab6d9c43
 * fixed some bugs causing conditions to not be displayed
 * fixed some trainer not being displayed for most skills
2020-02-23 17:29:12 +01:00
Sarjuuk
d4c0c0535a Quest/Breadcrumbs
* implemented https://github.com/TrinityCore/TrinityCore/commit/5ed77113b63
 * fixed some engrish
 * fixed a typo in typecasting string searches
2020-02-23 14:14:34 +01:00
Sarjuuk
eaa982e5b3 Totems/Display
* implment https://github.com/TrinityCore/TrinityCore/commit/915f8a9d2c2
2020-02-23 12:33:09 +01:00
Sarjuuk
48552a97ab Creature/Spells
* update db structure for https://github.com/TrinityCore/TrinityCore/commit/d5fb0a30ec6
2020-02-14 17:35:44 +01:00
Sarjuuk
243429bf68 SmartAI
* update db structure for https://github.com/TrinityCore/TrinityCore/commit/7634a57f64a
2020-02-14 16:54:36 +01:00
Sarjuuk
bc18ca174c Spells
* update db structure for https://github.com/TrinityCore/TrinityCore/commit/8edea4a3c29
2020-02-14 16:39:06 +01:00
jackpoz
1c08e5d9cf PHP/v7.3
* fixed warnings when migrating to php v7.3
 * https://www.php.net/manual/en/migration73.incompatible.php#migration73.incompatible.core.continue-targeting-switch
2020-02-14 16:26:38 +01:00
Sarjuuk
d3c5011694 Achievements
* update db structure for https://github.com/TrinityCore/TrinityCore/commit/352c4a4c680
2020-02-14 16:22:24 +01:00
Sarjuuk
c77cd92609 * updated footer to currentYear™
* revision push
2020-02-14 16:16:40 +01:00
jackpoz
1ff81ab07c Add support to https://github.com/TrinityCore/TrinityCore/pull/24102 2020-02-14 16:15:34 +01:00
Sarjuuk
1c7660316d Items/Gems
* fixed indexing error when calculating item enchantments introduced in c7fe84b7e0
2020-02-14 16:15:34 +01:00
wodim
eeb39bb83f Better wording for NPC loot containers 2020-02-14 15:19:02 +01:00
wodim
f99ff8c4b8 Fix typo 2020-02-14 15:19:02 +01:00
Sarjuuk
cf129ca3ce Pages/Quest
* escape htmlesque tags from quest titles for Infobox
2020-02-14 09:25:13 +01:00
Sarjuuk
fd04e9f977 Implemented new type: mail
* display and link clientside mails to other types and events
 * fixed favorites menu for new types
 * fixed sorting column triggered spells in enchantment listview
 * some misc cleanups
2020-02-14 00:36:41 +01:00
Sarjuuk
ccef11323b Setup/Scripts
* restructure setup to allow for self contained setup steps to self register (just the sql for now)
 * should ease adding new scripts in future
2020-02-14 00:36:41 +01:00
Sarjuuk
163e3d82b0 Spells/DetailPage
* fixed rogue reference causing created items to display as a different item

 thx @Rushor for pointing in the right direction
2020-02-12 23:59:35 +01:00
Sarjuuk
fc0902d476 Profiler/CSS
* fixed some batch-replace fails
 * year++++
2019-07-30 23:17:56 +02:00
Sarjuuk
0fd2944d8b Spells/DK
* fixed setup flagging wrong spells as hidden
 * fixed menu structure for DK spells not requiring DK class
 * fixed type for rune cost in spell tooltip
2019-01-04 17:19:14 +01:00
Sarjuuk
7f36dc87cf Mails
* apply table quest_mail_sender to display correct mail source
 * fixed mails from mailtemplate.dbc for achievement rewards
2018-12-15 00:21:52 +01:00
Sarjuuk
34fe4c2654 Creatures/Rank
* set unused ranks to 0 during setup to fix display issues
2018-12-14 23:30:26 +01:00
Sarjuuk
392610b899 Icons
* add icon to infobox for db types with icon (*duh!*)
 * fixed a couple issues with markup icondb in the process
 * fixed wrong data display due to shared name fields on joined table
2018-12-14 23:09:32 +01:00
Sarjuuk
ead6e72668 Page/Currency
* display price column in "currency for" tab
2018-12-11 19:55:02 +01:00
Sarjuuk
db012cfa8c Page/Search
* dropped support of wildcard characters
 * search for literal usage of underscore
2018-12-07 22:09:52 +01:00
Sarjuuk
09176d1ae9 Profiler/Names
* handle duplicate names due to rename flags more gracefully
 * should fix infinite reload bug on newly renamed profies
2018-12-02 19:25:43 +01:00
Sarjuuk
972a7f241e Misc/Fixes
* cast GET-params to lower case
 * fixed typo in constant name
 * fixed scope issue when aggregating sql stats
2018-12-02 19:22:21 +01:00
Sarjuuk
da1946df0f Misc/Fixes
* resolve two additional spell effects (11, 44)
 * fixed overzealous find/replace in Loot::iterate()
 * fixed wrong placeholder for type 32 in Conditions System
2018-12-02 00:55:34 +01:00
Sarjuuk
cc5be5261c Setup/DBconfig
* remove nonfunctional placeholder file
 * its too confusing
2018-12-01 20:03:26 +01:00
Sarjuuk
af7b9f57b0 Page/Search
* always display tab to denote what type was found
2018-11-30 18:33:59 +01:00
Sarjuuk
d9cd24026c PHP/Core
* enforced v7.1 compliance, preparations for v7.2
 * removed deprecated usage of each()
 * prefer array deconstruction via [] instead of calling list()
 * try to catch failed session_start()
 * prefer ... - Token instead of calling func_get_args() func_num_args()
 * enforce return types in AjaxHandler
 * revision push
2018-11-29 00:45:19 +01:00
Sarjuuk
f8a34aa98e Account/Cookies
* fixed saving cookie-data to account
 * e.g. announcements shouldn't pop up again if you close them once
2018-11-27 00:01:06 +01:00
Sarjuuk
13db19c64f Spells/Models
* fixed assigning same display to multiple spells from the same SpellList
 * fixes missing mounts in Profiler
2018-11-26 23:44:46 +01:00
Sarjuuk
04209cfc6d PHP-Version
* return type declarations require php v7.0 or newer
 * updated README and and version check accordingly
2018-11-20 19:00:20 +01:00
Sarjuuk
470498f63c Page/Achievement
* catch fatal error for achievements without criteria
2018-11-18 16:17:11 +01:00
Sarjuuk
484944bfc0 SmartAI
* parse and verbosely display smartAI for creatures, gameobjects and areatrigger
2018-11-18 15:31:22 +01:00
Sarjuuk
8620cbcf20 Setup/Quests
* update structure for TrinityCore/TrinityCore@2f5403d4af (Thx @TheWinchesters for noticing)
 * updated README accordingly
2018-11-18 15:31:14 +01:00
Sarjuuk
0286cb20f1 Spell/TaxiPath
* resolve taxi path routes on spells
 * fixed display of taxi paths on maps
2018-10-07 12:55:38 +02:00
Sarjuuk
31ec17d279 Pages/Quest
* escape placeholder tags in quest name for display
2018-08-26 16:49:29 +02:00
Sarjuuk
6f578b31e0 Pages/Achivement
* resorted criteria to match ingame order
2018-08-26 16:41:14 +02:00
Sarjuuk
0fd0a66137 Lang/Races
Hey Lang, what races does this thing have assigned?
  Yes!

   .. this will no longer happen
2018-08-16 21:49:16 +02:00
Sarjuuk
ceedf3fbdd Search
* made racial traits searchable under abilities
2018-08-11 23:13:09 +02:00
Sarjuuk
ed614d3938 Objects/Filters
* added filter for common spell foci
2018-07-29 22:41:32 +02:00
Sarjuuk
d32f21f9ca Spells/DetailPage
* display spells affectged by category cooldowns

Items/DetailPage
 * display items affected by category cooldowns defined in spell template
2018-07-20 17:34:33 +02:00
Sarjuuk
e0150feda6 implemented type areatrigger
* staff only
2018-07-16 21:08:18 +02:00
Sarjuuk
56ccc592a6 Sounds/DetailPage
* fixed generating links to play via the WoW API
 * fixed usage of not yet available PlaySoundKitID() when selecting file from list
 * while internally it is pretty laissez-faire about cases in file paths, suddenly WoW DOES care, when it comes to its LUA API.
   so .. don't cast to lowercase when storing sounds in DB
2018-06-11 23:17:55 +02:00
Sarjuuk
04e183f5e3 Spells/Filters
* add various filters (mostly attribute flags)
 * resolve additional dbc-data on detail page
 * fixed DK rune indexing
2018-06-06 22:31:56 +02:00
Sarjuuk
adc1273b08 Ajax/Debug
* be a lot more verbose when errors are occuring (to staff anyway)
 * made hardcoded error messages for comments localizable
 * add error messages from posting comments to session to be displayed on next page update
2018-05-26 21:02:19 +02:00
Sarjuuk
93a72013b8 Reports
* added timestamp column to db table
 * consolidated write operation
 * fixed storing reports when get_browser() would return empty
2018-05-26 15:59:01 +02:00
Sarjuuk
c5d28a9bbc Profiler/Chars
* prefix characters flagged for rename to prevent collision with real char names (multiple chars flagged for rename will still collide, but what the hoo haa)
 * alter table index to prevent creation and subsequent conflict with duplicate entries of the same char (realm/guid pair)
2018-05-26 13:29:23 +02:00
Sarjuuk
7dc283f649 Misc/Fixups
* squash multiple notices from error log
 * fixed copy&paste error preventing view on custom profiles
 * fixed display of spell requirements if mount was required after changs to mount sorting
 * properly get/send error code in profile synchronisation
 * use clear:left after floating mail block in template
2018-05-26 12:18:02 +02:00
Sarjuuk
9645ad0877 Localization/deDE
* kitsch is inappropriate???
2018-05-13 14:41:23 +02:00
Sarjuuk
45f325a0fa Admin/WeightPresets
* removed redundant code
 * fixed empty scales not being written to dataset as expected
2018-05-13 12:41:54 +02:00
Sarjuuk
37652ce011 Profiler/Raids
* fixed displaying individual encounters in raid activity tracker
2018-05-13 12:24:00 +02:00
Sarjuuk
594b2269bd Profiler/Profiles
* fixed regular characters being hidden by default
2018-05-13 12:03:44 +02:00
Sarjuuk
55a554ca06 Spells/Tooltips
* remove level scaling from tooltips (for now)
2018-05-11 13:14:06 +02:00
Sarjuuk
a0198ae5f4 Spells/Mounts
* group mounts into ground, flying & misc.
 * display mount speed bonus in listview
2018-05-10 22:14:13 +02:00
Sarjuuk
3bbd7f97da Item/ExtendedCost
* display a "sells" entry for every combination of currencies the vendor accepts per item
 * display every cost combination in the quick infos on the item detail page
2018-05-10 20:04:24 +02:00
Sarjuuk
761da59ee9 Localization/Chinese
* added support for locale 4
* some strings are missing and will need to be translated at a later date
* thx @qyh214 for a lot of content

Co-authored-by: qyh214 <sandy0214qin@msn.com>
2018-05-08 20:36:52 +02:00
Sarjuuk
09d2013012 Misc/Fixups
* fixed typo inlocalization
 * have the profiler queue run at least one cycle so it can get registered as successfully started
 * a map popup will no longer scroll the page to bottom
 * replaced an input placeholder image with proper placeholder property
2018-05-07 22:51:18 +02:00
Sarjuuk
4f0d045ff4 Quests/Misc
* remove Uldaman from Eastern Kingdoms
 * add Crystalsong forest to Northrend
2018-05-05 17:42:24 +02:00
Sarjuuk
e2e23c430a Quests/Categories
* shuffled quests into a structure that seems sensible (menus should no longer point to dead pages or display 'undefined' links)
2018-05-01 19:09:38 +02:00
Sarjuuk
9609c93f8c Quest/Detail Page
* display mail attachments besides the actual mail instead of extra tab
2018-04-30 10:59:24 +02:00
Sarjuuk
35dc835c62 NPCs/Detail Page
* fixed vendors displaying the same sell price for every item
2018-04-28 13:32:16 +02:00
Sarjuuk
808bbcc2fd Profiler/Profiles
* do not display deleted custom profiles in their parent profiles quick info
2018-04-28 12:03:52 +02:00
Sarjuuk
c5e9762830 Profiler/Profiles
* allow strings with less then 3 chars in name search
 * store name as binary to allow accent-sensitive search
   (note: guilds and arenateams have the same issue but utf8mb4_bin can't be applied blindly as the search also becomes case-sensitive (wich is already handled for profiles))
2018-04-28 11:32:22 +02:00
Sarjuuk
b13f3af03e Spell/Detail Page
* display effective MiscValue for effect PowerBurn
2018-04-28 10:57:41 +02:00
Sarjuuk
000a3208c0 Items/Armor
* fixed handling of additional Armor
2018-04-28 10:55:25 +02:00
Sarjuuk
efd68bdb5f Profiler
* fixed possible bug where a custom profile would load instead of the original character it was created from
2018-04-22 12:02:53 +02:00
Sarjuuk
d0b7fa9ef5 Profiler
* reworked default exclusions (hid some things entirely, flagged others for appropriate cases)
 * char names consisting of two letters are now valid
 * fixed matching enchantments onto ranged weapons
2018-04-22 11:48:30 +02:00
Sarjuuk
9dcb0b3a15 Profiler/Mounts
* display class-specific mounts on their respective classes
2018-04-08 11:28:46 +02:00
Sarjuuk
2518e4730b BaseType
* revert change to get number total results
   this partially reverts bf42973c
2018-04-07 20:52:10 +02:00
Sarjuuk
74308da407 Setup/Spells
* hide a lot of internal spells from searches
 * apply a faction to mount listing, so the Profiler can hide mounts from the opposite faction
 * apply GS to profession enchantments
2018-04-07 20:34:01 +02:00
Sarjuuk
bb9e6a36dd pages/Search
* fix searching by stat weights (used in Profiler, Item Comparison)
2018-04-07 20:31:50 +02:00
Sarjuuk
c448207724 JS/Locales
* grep-fix multiple issues with replacement patterns and hrefs
2018-04-07 20:30:40 +02:00
Sarjuuk
81335ad6dc Profiler/Reputation
* add missing base values for each race/class
2018-04-02 15:18:28 +02:00
Sarjuuk
d46a78b3cb Misc/Typos
* fixed display of charges on GameObject Infobox
 * removed display of achievement points as cost from vendors
2018-04-01 12:12:43 +02:00
Sarjuuk
492b7cd3c4 Profiler/Tooltips
* fixed display of titles
 * supply iconstring in the tooltip data as this does not rely on variables within global.js
   fixes externally embeded profile tooltips
2018-03-31 23:59:27 +02:00
Sarjuuk
c19691033a Profiler/Raid-Tracker
* sort raids by difficulty to fix progress bars
 * rename raids to include raid sizes
2018-03-31 17:25:11 +02:00
Sarjuuk
164cdc234c Profiler
* fixed directly accessing existing, but not yet fetched, profile
2018-03-31 17:21:24 +02:00
Sarjuuk
394a2699d8 Profiler
* translate anouncements for loc 3
 * fix queue locking up on error
 * fix queue status display
 * fix flagging real characters as private
2018-03-31 17:21:24 +02:00
Sarjuuk
e50333a518 DB/Favorites
* implement favorites for DB entries
 * click on the star besides the name of the entry to add it to a new quick menu
2018-03-30 19:22:45 +02:00
Sarjuuk
d848d316fe Misc/Typo 2018-03-29 23:52:02 +02:00
Sarjuuk
fa46aefa27 Modelviewer
* removed quality options (java .. holy crap!)
 * added animations menu
2018-03-29 21:19:36 +02:00
Sarjuuk
0912248fd5 Misc/Typos 2018-03-29 18:59:09 +02:00
Sarjuuk
fab71f9325 Misc
* don't cache playlists
 * don't cache new custom profiles
 * forgot to sanitize and use param from js
2018-03-29 14:40:46 +02:00
Sarjuuk
51eda12099 Pages/Home
* implement random home titles
2018-03-29 13:16:23 +02:00
Sarjuuk
bf42973c00 Profiler
* further optimize search
   - use achievement 4496 as shortcut for everything based around total achievement points
   - get talent distribution separately
   - get total profiler-items found separately
   - opt to not sort found results
 * fixed Profiles with zero Achievement Points
 * added cache key genrator i forgot :<
 * fixed typos
2018-03-29 12:47:27 +02:00
Sarjuuk
431e984f03 DB/Structure
* update collate to use utf8mb4
 * fixed creature paths being capped at 255 waypoints
2018-03-28 11:59:13 +02:00
Sarjuuk
e973e5e33b Profiler/RaidTracker
* ALL the Raids .. track em
2018-03-28 11:55:06 +02:00
Sarjuuk
22d02378ef Config/Profiler
* update config to enable/disable profiler in general, instead of just the queue
 * regenerate affected files on config change
2018-03-27 12:41:32 +02:00
Sarjuuk
c17cf9c043 Profiler/Profile
* shorten raid activity string for locale deDE to prevent icons from showing up where they shouldn't (also needed for locale ruRU)
 * define offhand frill as unfit for enchantments
 * resort raid activity tracker and add placeholder for ulduar
2018-03-27 00:03:56 +02:00
Sarjuuk
72c1dacd3f Profiler/Guilds
* fixed forcing guildnames to string for js
 * made static string localized
2018-03-26 22:09:53 +02:00
Sarjuuk
bc834245d7 Setup/Sounds
* make log more clear, that every available locale is checked
2018-03-26 19:58:32 +02:00
1038 changed files with 135250 additions and 80981 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

22
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,22 @@
---
name: Bug report
about: issue template
title: ''
labels: ''
assignees: ''
---
**Describe the bug and how to reproduce it**
additionally paste relevant lines from db table `aowow_errors`
or your browsers console here.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**System:**
- OS: [e.g. Win10]
- PHP version:
- revision used:
- Browser (in case of JavaScript / display errors):
- AzerothCore: yes/no

8
.gitignore vendored
View File

@@ -3,15 +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/global.js
/static/widgets/power.js
/static/widgets/power/demo.html
/static/widgets/searchbox.js
@@ -49,4 +49,4 @@
/static/uploads/screenshots/*
/static/uploads/signatures/*
/static/uploads/temp/*
/static/uploads/guide/images/*

View File

@@ -13,30 +13,34 @@ 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 ≥ 5.5.0 including extensions:
+ SimpleXML
+ GD
+ Mysqli
+ mbString
+ MySQL ≥ 5.5.30
+ [TDB 335.63](https://github.com/TrinityCore/TrinityCore/releases/tag/TDB335.63) - including world updates up to 04.05.2017
+ 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.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) / [BLPConverter](https://github.com/Sarjuuk/BLPConverter) (optional)
+ [MPQExtractor](https://github.com/Sarjuuk/MPQExtractor) / [FFmpeg](https://ffmpeg.org/download.html) / (optional: [BLPConverter](https://github.com/Sarjuuk/BLPConverter))
+ WIN users may find it easier to use these alternatives
+ [MPQEditor](http://www.zezula.net/en/mpq/download.html) / [FFmpeg](http://ffmpeg.zeranoe.com/builds/) / [BLPConverter](https://github.com/PatrickCyr/BLPConverter) (optional)
+ [MPQEditor](http://www.zezula.net/en/mpq/download.html) / [FFmpeg](http://ffmpeg.zeranoe.com/builds/) / (optional: [BLPConverter](https://github.com/PatrickCyr/BLPConverter))
audio processing may require [lame](https://sourceforge.net/projects/lame/files/lame/3.99/) or [vorbis-tools](https://www.xiph.org/downloads/) (which may require libvorbis (which may require libogg))
#### Highly Recommended
+ setting the following configuration values on your TrintyCore server will greatly increase the accuracy of spawn points
+ 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
@@ -47,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
@@ -69,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`.
@@ -89,7 +93,7 @@ WAV-files need to be reencoded as `ogg/vorbis` and some MP3s may identify themse
* [example for \*nix](https://gist.github.com/Sarjuuk/1f05ef2affe49a7e7ca0fad7b01c081d)
#### 6. Run the initial setup from the CLI
`php aowow --firstrun`.
`php aowow --setup`.
This should guide you through with minimal input required from your end, but will take some time though, especially compiling the zone-images. Use it to familiarize yourself with the other functions this setup has. Yes, I'm dead serious: *Go read the code!* It will help you understand how to configure AoWoW and keep it in sync with your world database.
When you've created your admin account you are done.
@@ -97,35 +101,41 @@ 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.
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 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.
Q: An Item, Quest or NPC i added or edited can't be searched. Why?
A: A search is only conducted against the currently used locale. You may have only edited the name field in the base table instead of adding multiple strings into the appropriate \*_locale tables. In this case searches in a non-english locale are run against an empty name field.
## Thanks
@mix: for providing the php-script to parse .blp and .dbc into usable images and tables
@LordJZ: the wrapper-class for DBSimple; the basic idea for the user-class
@kliver: basic implementation of screenshot uploads
@mix: for providing the php-script to parse .blp and .dbc into usable images and tables
@LordJZ: the wrapper-class for DBSimple; the basic idea for the user-class
@kliver: basic implementation of screenshot uploads
@Sarjuuk: maintainer of the project
## 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

@@ -1,49 +0,0 @@
<?php
if (!defined('AOWOW_REVISION'))
die('illegal access');
// use as example. File is generated from setup.
// -- Aowow Database --
// contains world-data, user-data and logs
$AoWoWconf['aowow'] = array(
'host' => '127.0.0.1',
'user' => '<user>',
'pass' => '<pass>',
'db' => 'world',
'prefix' => 'aowow_'
);
// -- World Database --
// used to generate data-tables
$AoWoWconf['world'] = array(
'host' => '127.0.0.1',
'user' => '<user>',
'pass' => '<pass>',
'db' => 'world',
'prefix' => ''
);
// -- Auth Database --
// used to generate user-tables
$AoWoWconf['auth'] = array(
'host' => '127.0.0.1',
'user' => '<user>',
'pass' => '<pass>',
'db' => 'auth',
'prefix' => ''
);
// -- Characters Database --
// used to display profiles
$AoWoWconf['characters'][<realmId>] = array(
'host' => '127.0.0.1',
'user' => '<user>',
'pass' => '<pass>',
'db' => 'characters',
'prefix' => ''
);
?>

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 ?? []);
}
}
?>

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