PageRenderTime 411ms CodeModel.GetById 28ms RepoModel.GetById 2ms app.codeStats 0ms

/cgi-bin/LJ/Tags.pm

https://github.com/stormerider/dw-free
Perl | 1632 lines | 1485 code | 55 blank | 92 comment | 62 complexity | 7357bb5887fa6d74f17c7e042b24cb43 MD5 | raw file
Possible License(s): GPL-2.0, BSD-3-Clause, LGPL-2.1

Large files files are truncated, but you can click here to view the full file

  1. # This code was forked from the LiveJournal project owned and operated
  2. # by Live Journal, Inc. The code has been modified and expanded by
  3. # Dreamwidth Studios, LLC. These files were originally licensed under
  4. # the terms of the license supplied by Live Journal, Inc, which can
  5. # currently be found at:
  6. #
  7. # http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
  8. #
  9. # In accordance with the original license, this code and all its
  10. # modifications are provided under the GNU General Public License.
  11. # A copy of that license can be found in the LICENSE file included as
  12. # part of this distribution.
  13. package LJ::Tags;
  14. use strict;
  15. use LJ::Global::Constants;
  16. use LJ::Lang;
  17. # <LJFUNC>
  18. # name: LJ::Tags::get_usertagsmulti
  19. # class: tags
  20. # des: Gets a bunch of tags for the specified list of users.
  21. # args: opts?, uobj*
  22. # des-opts: Optional hashref with options. Keys can be 'no_gearman' to skip gearman
  23. # task dispatching.
  24. # des-uobj: One or more user ids or objects to load the tags for.
  25. # returns: Hashref; { userid => *tagref*, userid => *tagref*, ... } where *tagref* is the
  26. # return value of LJ::Tags::get_usertags -- undef on failure
  27. # </LJFUNC>
  28. sub get_usertagsmulti {
  29. return {} unless LJ::is_enabled('tags');
  30. # options if provided
  31. my $opts = {};
  32. $opts = shift if ref $_[0] eq 'HASH';
  33. # get input users
  34. my @uobjs = grep { defined } map { LJ::want_user($_) } @_;
  35. return {} unless @uobjs;
  36. # now setup variables we'll need
  37. my @memkeys; # memcache keys to fetch
  38. my $res = {}; # { jid => { tagid => {}, ... }, ... }; results return hashref
  39. my %need; # ( jid => 0/1 ); whether we need tags for this user
  40. # prepopulate our structures
  41. foreach my $u (@uobjs) {
  42. # don't load if we've previously gotten this one
  43. if (my $cached = $LJ::REQ_CACHE_USERTAGS{$u->{userid}}) {
  44. $res->{$u->{userid}} = $cached;
  45. next;
  46. }
  47. # setup that we need this one
  48. $need{$u->{userid}} = $u;
  49. push @memkeys, [ $u->{userid}, "tags:$u->{userid}" ];
  50. }
  51. return $res unless @memkeys;
  52. # gather data from memcache if available
  53. my $memc = LJ::MemCache::get_multi(@memkeys) || {};
  54. foreach my $key (keys %$memc) {
  55. if ($key =~ /^tags:(\d+)$/) {
  56. my $jid = $1;
  57. # set this up in our return hash and mark unneeded
  58. $LJ::REQ_CACHE_USERTAGS{$jid} = $memc->{$key};
  59. $res->{$jid} = $memc->{$key};
  60. delete $need{$jid};
  61. }
  62. }
  63. return $res unless %need;
  64. # if we're not using gearman, or we're not in web context (implies that we're
  65. # in gearman context?) then we need to use the loader to get the data
  66. my $gc = LJ::gearman_client();
  67. return LJ::Tags::_get_usertagsmulti($res, values %need)
  68. unless LJ::conf_test($LJ::LOADTAGS_USING_GEARMAN, values %need) && $gc && ! $opts->{no_gearman};
  69. # spawn gearman jobs to get each of the users
  70. my $ts = $gc->new_task_set();
  71. foreach my $u (values %need) {
  72. $ts->add_task(Gearman::Task->new("load_usertags", \"$u->{userid}",
  73. {
  74. uniq => '-',
  75. on_complete => sub {
  76. my $resp = shift;
  77. my $tags = Storable::thaw($$resp);
  78. return unless $tags;
  79. $LJ::REQ_CACHE_USERTAGS{$u->{userid}} = $tags;
  80. $res->{$u->{userid}} = $tags;
  81. delete $need{$u->{userid}};
  82. },
  83. }));
  84. }
  85. # now wait for gearman to finish, then we're done
  86. $ts->wait(timeout => 15);
  87. return $res;
  88. }
  89. # internal sub used by get_usertagsmulti
  90. sub _get_usertagsmulti {
  91. my ($res, @uobjs) = @_;
  92. return $res unless @uobjs;
  93. # now setup variables we'll need
  94. my @memkeys; # memcache keys to fetch
  95. my %jid2cid; # ( jid => cid ); cross reference journals to clusters
  96. my %need; # ( cid => { jid => 0/1 } ); whether we need tags for this user
  97. my %need_kws; # ( cid => { jid => 0/1 } ); whether we need keywords for this user
  98. my %kws; # ( jid => { kwid => keyword, ... } ); keywords for a user
  99. my %dbcrs; # ( cid => dbcr ); stores database handles
  100. # prepopulate our structures
  101. foreach my $u (@uobjs) {
  102. # we will have to load these
  103. $jid2cid{$u->{userid}} = $u->{clusterid};
  104. $need{$u->{clusterid}}->{$u->{userid}} = 1;
  105. $need_kws{$u->{clusterid}}->{$u->{userid}} = 1;
  106. push @memkeys, [ $u->{userid}, "kws:$u->{userid}" ];
  107. }
  108. # gather data from memcache if available
  109. my $memc = LJ::MemCache::get_multi(@memkeys) || {};
  110. foreach my $key (keys %$memc) {
  111. if ($key =~ /^kws:(\d+)$/) {
  112. my $jid = $1;
  113. my $cid = $jid2cid{$jid};
  114. # save for later and mark unneeded
  115. $kws{$jid} = $memc->{$key};
  116. delete $need_kws{$cid}->{$jid};
  117. delete $need_kws{$cid} unless %{$need_kws{$cid}};
  118. }
  119. }
  120. # get keywords first
  121. foreach my $cid (keys %need_kws) {
  122. next unless %{$need_kws{$cid}};
  123. # get db for this cluster
  124. my $dbcr = ($dbcrs{$cid} ||= LJ::get_cluster_def_reader($cid))
  125. or next;
  126. # get the keywords from the database
  127. my $in = join(',', map { $_ + 0 } keys %{$need_kws{$cid}});
  128. my $kwrows = $dbcr->selectall_arrayref("SELECT userid, kwid, keyword FROM userkeywords WHERE userid IN ($in)");
  129. next if $dbcr->err || ! $kwrows;
  130. # break down into data structures
  131. my %keywords; # ( jid => { kwid => keyword } )
  132. $keywords{$_->[0]}->{$_->[1]} = $_->[2]
  133. foreach @$kwrows;
  134. next unless %keywords;
  135. # save and store to memcache
  136. foreach my $jid (keys %keywords) {
  137. $kws{$jid} = $keywords{$jid};
  138. LJ::MemCache::add([ $jid, "kws:$jid" ], $keywords{$jid});
  139. }
  140. }
  141. # now, what we need per cluster...
  142. foreach my $cid (keys %need) {
  143. next unless %{$need{$cid}};
  144. # get db for this cluster
  145. my $dbcr = ($dbcrs{$cid} ||= LJ::get_cluster_def_reader($cid))
  146. or next;
  147. my @all_jids = map { $_ + 0 } keys %{$need{$cid}};
  148. # get the tags from the database
  149. my $in = join(',', @all_jids);
  150. my $tagrows = $dbcr->selectall_arrayref("SELECT journalid, kwid, parentkwid, display FROM usertags WHERE journalid IN ($in)");
  151. next if $dbcr->err;
  152. # break down into data structures
  153. my %tags; # ( jid => { kwid => display } )
  154. $tags{$_->[0]}->{$_->[1]} = $_->[3]
  155. foreach @$tagrows;
  156. # now turn this into a tentative results hash: { userid => { tagid => { name => tagname, ... }, ... } }
  157. # this is done by combining the information we got from the tags lookup along with
  158. # the stuff from the keyword lookup. we need the relevant rows from both sources
  159. # before they appear in this hash.
  160. foreach my $jid (keys %tags) {
  161. next unless $kws{$jid};
  162. foreach my $kwid (keys %{$tags{$jid}}) {
  163. $res->{$jid}->{$kwid} =
  164. {
  165. name => $kws{$jid}->{$kwid},
  166. security => {
  167. public => 0,
  168. groups => {},
  169. private => 0,
  170. protected => 0
  171. },
  172. uses => 0,
  173. display => $tags{$jid}->{$kwid},
  174. };
  175. }
  176. }
  177. # get security counts
  178. my @resjids = keys %$res;
  179. my $ids = join(',', map { $_+0 } @resjids);
  180. my $counts = [];
  181. # populate security counts
  182. if ( @resjids ) {
  183. $counts = $dbcr->selectall_arrayref("SELECT journalid, kwid, security, entryct FROM logkwsum WHERE journalid IN ($ids)");
  184. next if $dbcr->err;
  185. }
  186. # setup some helper values
  187. my $public_mask = 1 << 63;
  188. my $trust_mask = 1 << 0;
  189. # melt this information down into the hashref
  190. foreach my $row (@$counts) {
  191. my ($jid, $kwid, $sec, $ct) = @$row;
  192. # make sure this journal and keyword are present in the results already
  193. # so we don't auto-vivify something with security that has no keyword with it
  194. next unless $res->{$jid} && $res->{$jid}->{$kwid};
  195. # add these to the total uses
  196. $res->{$jid}->{$kwid}->{uses} += $ct;
  197. if ($sec & $public_mask) {
  198. $res->{$jid}->{$kwid}->{security}->{public} += $ct;
  199. $res->{$jid}->{$kwid}->{security_level} = 'public';
  200. } elsif ($sec & $trust_mask) {
  201. $res->{$jid}->{$kwid}->{security}->{protected} += $ct;
  202. $res->{$jid}->{$kwid}->{security_level} = 'protected'
  203. unless $res->{$jid}->{$kwid}->{security_level} &&
  204. $res->{$jid}->{$kwid}->{security_level} eq 'public';
  205. } elsif ($sec) {
  206. # if $sec is true (>0), and not trust/public, then it's a group(s). but it's
  207. # still in the form of a number, and we want to know which group(s) it is. so
  208. # we must convert the mask back to a bit number with LJ::bit_breakdown.
  209. foreach my $grpid ( LJ::bit_breakdown($sec) ) {
  210. $res->{$jid}->{$kwid}->{security}->{groups}->{$grpid} += $ct;
  211. }
  212. $res->{$jid}->{$kwid}->{security_level} ||= 'group';
  213. } else {
  214. # $sec must be 0
  215. $res->{$jid}->{$kwid}->{security}->{private} += $ct;
  216. }
  217. }
  218. # default securities to private and store to memcache
  219. foreach my $jid (@all_jids) {
  220. $res->{$jid} ||= {};
  221. $res->{$jid}->{$_}->{security_level} ||= 'private'
  222. foreach keys %{$res->{$jid}};
  223. $LJ::REQ_CACHE_USERTAGS{$jid} = $res->{$jid};
  224. LJ::MemCache::add([ $jid, "tags:$jid" ], $res->{$jid});
  225. }
  226. }
  227. return $res;
  228. }
  229. # <LJFUNC>
  230. # name: LJ::Tags::get_usertags
  231. # class: tags
  232. # des: Returns the tags that a user has defined for their account.
  233. # args: uobj, opts?
  234. # des-uobj: User object to get tags for.
  235. # des-opts: Optional hashref; key can be 'remote' to filter tags to only ones that remote can see
  236. # returns: Hashref; key being tag id, value being a large hashref (FIXME: document)
  237. # </LJFUNC>
  238. sub get_usertags {
  239. return {} unless LJ::is_enabled('tags');
  240. my $u = LJ::want_user(shift)
  241. or return undef;
  242. my $opts = shift() || {};
  243. # get tags for this user
  244. my $tags = LJ::Tags::get_usertagsmulti($u);
  245. return undef unless $tags;
  246. # get the tags for this user
  247. my $res = $tags->{$u->{userid}} || {};
  248. return {} unless %$res;
  249. # now if they provided a remote, remove the ones they don't want to see; note that
  250. # remote may be undef so we have to check exists
  251. if ( exists $opts->{remote} ) {
  252. # never going to cull anything if you control it, so just return
  253. return $res if LJ::isu( $opts->{remote} ) && $opts->{remote}->can_manage( $u );
  254. # setup helper variables from u to remote
  255. my ($trusted, $grpmask) = (0, 0);
  256. if ($opts->{remote}) {
  257. $trusted = $u->trusts_or_has_member( $opts->{remote} );
  258. $grpmask = $u->trustmask( $opts->{remote} );
  259. }
  260. # figure out what we need to purge
  261. my @purge;
  262. TAG: foreach my $tagid (keys %$res) {
  263. my $sec = $res->{$tagid}->{security_level};
  264. next TAG if $sec eq 'public';
  265. next TAG if $trusted && $sec eq 'protected';
  266. if ($grpmask && $sec eq 'group') {
  267. foreach my $grpid (keys %{$res->{$tagid}->{security}->{groups}}) {
  268. next TAG if $grpmask & (1 << $grpid);
  269. }
  270. }
  271. push @purge, $tagid;
  272. }
  273. delete $res->{$_} foreach @purge;
  274. }
  275. return $res;
  276. }
  277. # <LJFUNC>
  278. # name: LJ::Tags::get_entry_tags
  279. # class: tags
  280. # des: Gets tags that have been used on an entry.
  281. # args: uuserid, jitemid
  282. # des-uuserid: User id or object of account with entry
  283. # des-jitemid: Journal itemid of entry; may also be arrayref of jitemids in journal.
  284. # returns: Hashref; { jitemid => { tagid => tagname, tagid => tagname, ... }, ... }
  285. # </LJFUNC>
  286. sub get_logtags {
  287. return {} unless LJ::is_enabled('tags');
  288. my $u = LJ::want_user(shift);
  289. return undef unless $u;
  290. # handle magic jitemid parameter
  291. my $jitemid = shift;
  292. unless (ref $jitemid eq 'ARRAY') {
  293. $jitemid = [ $jitemid+0 ];
  294. return undef unless $jitemid->[0];
  295. }
  296. return undef unless @$jitemid;
  297. # transform to a call to get_logtagsmulti
  298. my $ret = LJ::Tags::get_logtagsmulti({ $u->{clusterid} => [ map { [ $u->{userid}, $_ ] } @$jitemid ] });
  299. return undef unless $ret && ref $ret eq 'HASH';
  300. # now construct result hashref
  301. return { map { $_ => $ret->{"$u->{userid} $_"} } @$jitemid };
  302. }
  303. # <LJFUNC>
  304. # name: LJ::Tags::get_logtagsmulti
  305. # class: tags
  306. # des: Load tags on a given set of entries
  307. # args: idsbycluster
  308. # des-idsbycluster: { clusterid => [ [ jid, jitemid ], [ jid, jitemid ], ... ] }
  309. # returns: hashref with "jid jitemid" keys, value of each being a hashref of
  310. # { tagid => tagname, ... }
  311. # </LJFUNC>
  312. sub get_logtagsmulti {
  313. return {} unless LJ::is_enabled('tags');
  314. # get parameter (only one!)
  315. my $idsbycluster = shift;
  316. return undef unless $idsbycluster && ref $idsbycluster eq 'HASH';
  317. # the mass of variables to make this mess work!
  318. my @jids; # journalids we've seen
  319. my @memkeys; # memcache keys to load
  320. my %ret; # ( jid => { jitemid => [ tagid, tagid, ... ], ... } ); storage for data pre-final conversion
  321. my %set; # ( jid => ( jitemid => [ tagid, tagid, ... ] ) ); for setting in memcache
  322. my $res = {}; # { "jid jitemid" => { tagid => kw, tagid => kw, ... } }; final results hashref for return
  323. my %need; # ( cid => { jid => { jitemid => 1, jitemid => 1 } } ); what still needs loading
  324. my %jid2cid; # ( jid => cid ); map of journal id to clusterid
  325. # construct memcache keys for loading below
  326. foreach my $cid (keys %$idsbycluster) {
  327. foreach my $row (@{$idsbycluster->{$cid} || []}) {
  328. $need{$cid}->{$row->[0]}->{$row->[1]} = 1;
  329. $jid2cid{$row->[0]} = $cid;
  330. $set{$row->[0]}->{$row->[1]} = []; # empty initially
  331. push @memkeys, [ $row->[0], "logtag:$row->[0]:$row->[1]" ];
  332. }
  333. }
  334. # now hit up memcache to try to find what we can
  335. my $memc = LJ::MemCache::get_multi(@memkeys) || {};
  336. foreach my $key (keys %$memc) {
  337. if ($key =~ /^logtag:(\d+):(\d+)$/) {
  338. my ($jid, $jitemid) = ($1, $2);
  339. my $cid = $jid2cid{$jid};
  340. # save memcache output hashref to out %ret var
  341. $ret{$jid}->{$jitemid} = $memc->{$key};
  342. # remove the need to prevent loading from the database and storage to memcache
  343. delete $need{$cid}->{$jid}->{$jitemid};
  344. delete $need{$cid}->{$jid} unless %{$need{$cid}->{$jid}};
  345. delete $need{$cid} unless %{$need{$cid}};
  346. }
  347. }
  348. # iterate over clusters and construct SQL to get the data...
  349. foreach my $cid (keys %need) {
  350. my $dbcm = LJ::get_cluster_master($cid)
  351. or return undef;
  352. # list of (jid, jitemid) pairs that we get from %need
  353. my @where;
  354. foreach my $jid (keys %{$need{$cid} || {}}) {
  355. my @jitemids = keys %{$need{$cid}->{$jid} || {}};
  356. next unless @jitemids;
  357. push @where, "(journalid = $jid AND jitemid IN (" . join(",", @jitemids) . "))";
  358. }
  359. # prepare the query to run
  360. my $where = join(' OR ', @where);
  361. my $rows = $dbcm->selectall_arrayref("SELECT journalid, jitemid, kwid FROM logtags WHERE $where");
  362. return undef if $dbcm->err || ! $rows;
  363. # get data into %set so we add it to memcache later
  364. push @{$set{$_->[0]}->{$_->[1]} ||= []}, $_->[2] foreach @$rows;
  365. }
  366. # now add the things to memcache that we loaded from the clusters and also
  367. # transport them into the $ret hashref or returning to the user
  368. foreach my $jid (keys %set) {
  369. foreach my $jitemid (keys %{$set{$jid}}) {
  370. next unless $need{$jid2cid{$jid}}->{$jid}->{$jitemid};
  371. LJ::MemCache::add([ $jid, "logtag:$jid:$jitemid" ], $set{$jid}->{$jitemid});
  372. $ret{$jid}->{$jitemid} = $set{$jid}->{$jitemid};
  373. }
  374. }
  375. # quickly load all tags for the users we've found
  376. @jids = keys %ret;
  377. my $utags = LJ::Tags::get_usertagsmulti(@jids);
  378. return undef unless $utags;
  379. # last step: convert keywordids to keywords
  380. foreach my $jid (@jids) {
  381. my $tags = $utags->{$jid};
  382. next unless $tags;
  383. # transpose data from %ret into $res hashref which has (kwid => keyword) pairs
  384. foreach my $jitemid (keys %{$ret{$jid}}) {
  385. $res->{"$jid $jitemid"}->{$_} = $tags->{$_}->{name}
  386. foreach @{$ret{$jid}->{$jitemid} || []};
  387. }
  388. }
  389. # finally return the result hashref
  390. return $res;
  391. }
  392. # <LJFUNC>
  393. # name: LJ::Tags::can_add_tags
  394. # class: tags
  395. # des: Determines if one account is allowed to add tags to another's entry.
  396. # args: u, remote
  397. # des-u: User id or object of account tags are being added to
  398. # des-remote: User id or object of account performing the action
  399. # returns: 1 if allowed, 0 if not, undef on error
  400. # </LJFUNC>
  401. sub can_add_tags {
  402. return undef unless LJ::is_enabled('tags');
  403. my $u = LJ::want_user(shift);
  404. my $remote = LJ::want_user(shift);
  405. return undef unless $u && $remote;
  406. # we don't allow identity users to add tags, even when tag permissions would otherwise allow any user on the site
  407. # exception are communities that explicitly allow identity users to post in them
  408. # FIXME: perhaps we should restrict on all users, but allow for more restrictive settings such as members?
  409. return undef unless $remote->is_personal || $remote->is_identity && $u->prop( 'identity_posting' );
  410. return undef if $u->has_banned( $remote );
  411. # get permission hashref and check it; note that we fall back to the control
  412. # permission, which will allow people to add even if they can't add by default
  413. my $perms = LJ::Tags::get_permission_levels($u);
  414. return LJ::Tags::_remote_satisfies_permission($u, $remote, $perms->{add}) ||
  415. LJ::Tags::_remote_satisfies_permission($u, $remote, $perms->{control});
  416. }
  417. sub can_add_entry_tags {
  418. return undef unless LJ::is_enabled( "tags" );
  419. my ( $remote, $entry ) = @_;
  420. $remote = LJ::want_user( $remote );
  421. return undef unless $remote && $entry;
  422. my $journal = $entry->journal;
  423. return undef unless $remote->is_personal || $remote->is_identity && $journal->prop( 'identity_posting' );
  424. return undef if $journal->has_banned( $remote );
  425. my $perms = LJ::Tags::get_permission_levels( $journal );
  426. # specific case: are we the author of this entry, or otherwise an admin of the journal?
  427. if ( $perms->{add} eq 'author_admin' ) {
  428. # is author
  429. return 1 if $remote->equals( $entry->poster );
  430. # is journal administrator
  431. return $remote->can_manage( $journal );
  432. }
  433. # general case, see if the remote can add tags to the journal, in general
  434. return 1 if $remote->can_add_tags_to( $journal );
  435. # not allowed
  436. return undef;
  437. }
  438. # <LJFUNC>
  439. # name: LJ::Tags::can_control_tags
  440. # class: tags
  441. # des: Determines if one account is allowed to control (add, edit, delete) the tags of another.
  442. # args: u, remote
  443. # des-u: User id or object of account tags are being edited on.
  444. # des-remote: User id or object of account performing the action.
  445. # returns: 1 if allowed, 0 if not, undef on error
  446. # </LJFUNC>
  447. sub can_control_tags {
  448. return undef unless LJ::is_enabled('tags');
  449. my $u = LJ::want_user(shift);
  450. my $remote = LJ::want_user(shift);
  451. return undef unless $u && $remote;
  452. return undef unless $remote->is_personal || $remote->is_identity && $u->prop( 'identity_posting' );
  453. return undef if $u->has_banned( $remote );
  454. # get permission hashref and check it
  455. my $perms = LJ::Tags::get_permission_levels($u);
  456. return LJ::Tags::_remote_satisfies_permission($u, $remote, $perms->{control});
  457. }
  458. # helper sub internal used by can_*_tags functions
  459. sub _remote_satisfies_permission {
  460. my ($u, $remote, $perm) = @_;
  461. return undef unless $u && $remote && $perm;
  462. # allow if they can manage it (own, or 'A' edge)
  463. return 1 if $remote->can_manage( $u );
  464. # permission checks
  465. if ($perm eq 'public') {
  466. return 1;
  467. } elsif ($perm eq 'none') {
  468. return 0;
  469. } elsif ( $perm eq 'protected' || $perm eq 'friends' ) { # 'friends' for backwards compatibility
  470. return $u->trusts_or_has_member( $remote );
  471. } elsif ($perm eq 'private') {
  472. return 0; # $remote->can_manage( $u ) already returned 1 above
  473. } elsif ( $perm eq 'author_admin' ) {
  474. # this tests whether the remote can add tags for this journal in general
  475. # when we don't have an entry object available to us (e.g., posting)
  476. # Existing entries, checking per-entry author permissions, should use
  477. # LJ::Tag::can_add_entry_tags
  478. return $remote->can_manage( $u ) || $remote->member_of( $u );
  479. } elsif ($perm =~ /^group:(\d+)$/) {
  480. my $grpid = $1+0;
  481. return undef unless $grpid >= 1 && $grpid <= 60;
  482. my $mask = $u->trustmask( $remote );
  483. return ($mask & (1 << $grpid)) ? 1 : 0;
  484. } else {
  485. # else, problem!
  486. return undef;
  487. }
  488. }
  489. # <LJFUNC>
  490. # name: LJ::Tags::get_permission_levels
  491. # class: tags
  492. # des: Gets the permission levels on an account.
  493. # args: uobj
  494. # des-uobj: User id or object of account to get permissions for.
  495. # returns: Hashref; keys one of 'add', 'control'; values being 'private' (only the account
  496. # in question), 'protected' (all trusted), 'public' (everybody), 'group:N' (one
  497. # trust group with given id), or 'none' (nobody can).
  498. # </LJFUNC>
  499. sub get_permission_levels {
  500. return { add => 'none', control => 'none' }
  501. unless LJ::is_enabled('tags');
  502. my $u = LJ::want_user(shift);
  503. return undef unless $u;
  504. # return defaults for accounts
  505. unless ( $u->prop( 'opt_tagpermissions' ) ) {
  506. if ( $u->is_community ) {
  507. # communities are members (trusted) add, private (maintainers) control
  508. return { add => 'protected', control => 'private' };
  509. } elsif ( $u->is_person ) {
  510. # people let trusted add, self control
  511. return { add => 'private', control => 'private' };
  512. } else {
  513. # other account types can't add tags
  514. return { add => 'none', control => 'none' };
  515. }
  516. }
  517. # now split and return
  518. my ($add, $control) = split(/\s*,\s*/, $u->{opt_tagpermissions});
  519. return { add => $add, control => $control };
  520. }
  521. # <LJFUNC>
  522. # name: LJ::Tags::is_valid_tagstring
  523. # class: tags
  524. # des: Determines if a string contains a valid list of tags.
  525. # args: tagstring, listref?, opts?
  526. # des-tagstring: Opaque tag string provided by the user.
  527. # des-listref: If specified, return valid list of canonical tags in arrayref here.
  528. # des-opts: currently only 'omit_underscore_check' is recognized
  529. # returns: 1 if list is valid, 0 if not.
  530. # </LJFUNC>
  531. sub is_valid_tagstring {
  532. my ($tagstring, $listref, $opts) = @_;
  533. return 0 unless $tagstring;
  534. $listref ||= [];
  535. $opts ||= {};
  536. # setup helper subs
  537. my $valid_tag = sub {
  538. my $tag = shift;
  539. # a tag that starts with an underscore is reserved for future use,
  540. # but we added this after some underscores already existed.
  541. # Allow underscore tags to be viewed/deleted, but not created/modified.
  542. return 0 if ! $opts->{'omit_underscore_check'} && $tag =~ /^_/;
  543. return 0 if $tag =~ /[\<\>\r\n\t]/; # no HTML, newlines, tabs, etc
  544. return 0 unless $tag =~ /^(?:.+\s?)+$/; # one or more "words"
  545. return 1;
  546. };
  547. my $canonical_tag = sub {
  548. my $tag = shift;
  549. $tag =~ s/\s+/ /g; # condense multiple spaces to a single space
  550. $tag = LJ::text_trim($tag, LJ::BMAX_KEYWORD, LJ::CMAX_KEYWORD);
  551. $tag = LJ::utf8_lc( $tag );
  552. return $tag;
  553. };
  554. # now iterate
  555. my @list = grep { length $_ } # only keep things that are something
  556. map { LJ::trim($_) } # remove leading/trailing spaces
  557. split(/\s*,\s*/, $tagstring); # split on comma with optional spaces
  558. return 0 unless @list;
  559. # now validate each one as we go
  560. foreach my $tag (@list) {
  561. # canonicalize and determine validity
  562. $tag = $canonical_tag->($tag);
  563. return 0 unless $valid_tag->($tag);
  564. # now push on our list
  565. push @$listref, $tag;
  566. }
  567. # well, it must have been okay if we got here
  568. return 1;
  569. }
  570. # <LJFUNC>
  571. # name: LJ::Tags::get_security_level
  572. # class: tags
  573. # des: Returns the security level that applies to the given security information.
  574. # args: security, allowmask
  575. # des-security: 'private', 'public', or 'usemask'
  576. # des-allowmask: a bitmask in standard allowmask form
  577. # returns: Bitwise security level to use for [dbtable[logkwsum]] table.
  578. # </LJFUNC>
  579. sub get_security_level {
  580. my ($sec, $mask) = @_;
  581. return 0 if $sec eq 'private';
  582. return 1 << 63 if $sec eq 'public';
  583. return $mask;
  584. }
  585. # <LJFUNC>
  586. # name: LJ::Tags::update_logtags
  587. # class: tags
  588. # des: Updates the tags on an entry. Tags not in the list you provide are deleted.
  589. # args: uobj, jitemid, tags, opts
  590. # des-uobj: User id or object of account with entry
  591. # des-jitemid: Journal itemid of entry to tag
  592. # des-tags: List of tags you want applied to entry.
  593. # des-opts: Hashref; keys being the action and values of the key being an arrayref of
  594. # tags to involve in the action. Possible actions are 'add', 'set', and
  595. # 'delete'. With those, the value is a hashref of the tags (textual tags)
  596. # to add, set, or delete. Other actions are 'add_ids', 'set_ids', and
  597. # 'delete_ids'. The value arrayref should then contain the tag ids to
  598. # act with. Can also specify 'add_string', 'set_string', or 'delete_string'
  599. # as a comma separated list of user-supplied tags which are then canonicalized
  600. # and used. 'remote' is the remote user taking the actions (required).
  601. # 'err_ref' is ref to scalar to return error messages in. optional, and may
  602. # not be set by all error conditions. 'ignore_max' if specified will ignore
  603. # a user's max tags limit.
  604. # returns: 1 on success, undef on error
  605. # </LJFUNC>
  606. sub update_logtags {
  607. return undef unless LJ::is_enabled('tags');
  608. my $u = LJ::want_user(shift);
  609. my $jitemid = shift() + 0;
  610. return undef unless $u && $jitemid;
  611. return undef unless $u->writer;
  612. # ensure we have an options hashref
  613. my $opts = shift;
  614. return undef unless $opts && ref $opts eq 'HASH';
  615. # setup error stuff
  616. my $err = sub {
  617. my $fake = "";
  618. my $err_ref = $opts->{err_ref} && ref $opts->{err_ref} eq 'SCALAR' ? $opts->{err_ref} : \$fake;
  619. $$err_ref = shift() || "Unspecified error";
  620. return undef;
  621. };
  622. # perform set logic?
  623. my $do_set = exists $opts->{set} || exists $opts->{set_ids} || exists $opts->{set_string};
  624. # now get extra options
  625. my $remote = LJ::want_user(delete $opts->{remote});
  626. return undef unless $remote || $opts->{force};
  627. # get trust levels
  628. my $entry = LJ::Entry->new( $u, jitemid => $jitemid );
  629. my $can_control = LJ::Tags::can_control_tags($u, $remote);
  630. my $can_add = $can_control || LJ::Tags::can_add_entry_tags( $remote, $entry );
  631. # bail out early if we can't do any actions
  632. return $err->( LJ::Lang::ml( 'taglib.error.access' ) )
  633. unless $can_add || $opts->{force};
  634. # load the user's tags
  635. my $utags = LJ::Tags::get_usertags($u);
  636. return undef unless $utags;
  637. # for errors that we want to skip over silently instead of failing, but still report at the end
  638. my @skippable_errors;
  639. my @unauthorized_add;
  640. # take arrayrefs of tag strings and stringify them for validation
  641. my @to_create;
  642. foreach my $verb (qw(add set delete)) {
  643. # if given tags, combine into a string
  644. if ($opts->{$verb}) {
  645. $opts->{"${verb}_string"} = join(', ', @{$opts->{$verb}});
  646. $opts->{$verb} = [];
  647. }
  648. # now validate the string, if we have one
  649. if ($opts->{"${verb}_string"}) {
  650. $opts->{$verb} = [];
  651. return $err->( LJ::Lang::ml( 'taglib.error.invalid', { tagname => LJ::ehtml( $opts->{"${verb}_string"} ) } ) )
  652. unless LJ::Tags::is_valid_tagstring($opts->{"${verb}_string"}, $opts->{$verb});
  653. }
  654. # and turn everything into ids
  655. $opts->{"${verb}_ids"} ||= [];
  656. foreach my $kw (@{$opts->{$verb} || []}) {
  657. my $kwid = $u->get_keyword_id( $kw, $can_control );
  658. if ($can_control) {
  659. # error if we failed to create
  660. return undef unless $kwid;
  661. } else {
  662. # if we're not creating, who cares, just skip; also skip if the keyword
  663. # is not really a tag (don't promote it)
  664. unless ( $kwid && $utags->{$kwid} ) {
  665. push @unauthorized_add, $kw;
  666. next;
  667. }
  668. }
  669. # add the ids to the list, and save to create later if needed
  670. push @to_create, $kw unless $utags->{$kwid};
  671. push @{$opts->{"${verb}_ids"}}, $kwid;
  672. }
  673. }
  674. # setup %add/%delete hashes, for easier duplicate removal
  675. my %add = ( map { $_ => 1 } @{$opts->{add_ids} || []} );
  676. my %delete = ( map { $_ => 1 } @{$opts->{delete_ids} || []} );
  677. # used to keep counts in sync
  678. my $tags = LJ::Tags::get_logtags($u, $jitemid);
  679. return undef unless $tags;
  680. # now get tags for this entry; which there might be none, so make it a hashref
  681. $tags = $tags->{$jitemid} || {};
  682. # set is broken down into add/delete as necessary
  683. if ($do_set || ($opts->{set_ids} && @{$opts->{set_ids}})) {
  684. # mark everything to delete, we'll fix it shortly
  685. $delete{$_} = 1 foreach keys %{$tags};
  686. # and now go through the set we want, things that are in the delete
  687. # pile are just nudge so we don't touch them, and everything else we
  688. # throw in the add pile
  689. foreach my $id (@{$opts->{set_ids}}) {
  690. $add{$id} = 1
  691. unless delete $delete{$id};
  692. }
  693. }
  694. # now don't readd things we already have
  695. delete $add{$_} foreach keys %{$tags};
  696. # populate the errref, but don't actually return.
  697. push @skippable_errors, LJ::Lang::ml( "taglib.error.add", { tags => join( ", ", @unauthorized_add ) } ) if @unauthorized_add;
  698. push @skippable_errors, LJ::Lang::ml( "taglib.error.delete", { tags => join( ", ", map { $utags->{$_}->{name} } keys %delete ) } ) if %delete && ! $can_control ;
  699. $err->( join " ", @skippable_errors ) if @skippable_errors;
  700. # but delete nothing if we're not a controller
  701. %delete = () unless $can_control || $opts->{force};
  702. # bail out if nothing needs to be done
  703. return 1 unless %add || %delete;
  704. # at this point we have enough information to determine if they're going to break their
  705. # max, so let's do that so we can bail early enough to prevent a rollback operation
  706. my $max = $opts->{ignore_max} ? 0 : $u->count_tags_max;
  707. if (@to_create && $max && $max > 0) {
  708. my $total = scalar(keys %$utags) + scalar(@to_create);
  709. if ( $total > $max ) {
  710. return $err->(LJ::Lang::ml('taglib.error.toomany3', { max => $max,
  711. excess => $total - $max }));
  712. }
  713. }
  714. # now we can create the new tags, since we know we're safe
  715. # We still need to propagate ignore_max, as create_usertag does some checks of it's own.
  716. LJ::Tags::create_usertag( $u, $_, { display => 1, ignore_max => $opts->{ignore_max} } ) foreach @to_create;
  717. # %add and %delete are accurate, but we need to track necessary
  718. # security updates; this is a hash of keyword ids and a modification
  719. # value (a delta; +/-N) to be applied to that row later
  720. my %security;
  721. # get the security of this post for use in %security; do this now so
  722. # we don't interrupt the transaction below
  723. my $l2row = LJ::get_log2_row($u, $jitemid);
  724. return undef unless $l2row;
  725. # calculate security masks
  726. my $sec = LJ::Tags::get_security_level($l2row->{security}, $l2row->{allowmask});
  727. # setup a rollback bail path so that we can undo everything we've done
  728. # if anything fails in the middle; and if the rollback fails, scream loudly
  729. # and burst into flames!
  730. my $rollback = sub {
  731. die $u->errstr unless $u->rollback;
  732. return undef;
  733. };
  734. # start the big transaction, for great justice!
  735. $u->begin_work;
  736. # process additions first
  737. my @bind;
  738. foreach my $kwid (keys %add) {
  739. $security{$kwid}++;
  740. push @bind, $u->{userid}, $jitemid, $kwid;
  741. }
  742. my $recentlimit = $LJ::RECENT_TAG_LIMIT || 500;
  743. # now add all to both tables; only do $recentlimit rows ($recentlimit * 3 bind vars) at a time
  744. while (my @list = splice(@bind, 0, 3 * $recentlimit)) {
  745. my $sql = join(',', map { "(?,?,?)" } 1..(scalar(@list)/3));
  746. $u->do("REPLACE INTO logtags (journalid, jitemid, kwid) VALUES $sql", undef, @list);
  747. return $rollback->() if $u->err;
  748. $u->do("REPLACE INTO logtagsrecent (journalid, jitemid, kwid) VALUES $sql", undef, @list);
  749. return $rollback->() if $u->err;
  750. }
  751. # now process deletions
  752. @bind = ();
  753. foreach my $kwid (keys %delete) {
  754. $security{$kwid}--;
  755. push @bind, $kwid;
  756. }
  757. # now run the SQL
  758. while (my @list = splice(@bind, 0, $recentlimit)) {
  759. my $sql = join(',', map { $_ + 0 } @list);
  760. $u->do("DELETE FROM logtags WHERE journalid = ? AND jitemid = ? AND kwid IN ($sql)",
  761. undef, $u->{userid}, $jitemid);
  762. return $rollback->() if $u->err;
  763. $u->do("DELETE FROM logtagsrecent WHERE journalid = ? AND kwid IN ($sql) AND jitemid = ?",
  764. undef, $u->{userid}, $jitemid);
  765. return $rollback->() if $u->err;
  766. }
  767. # now handle lazy cleaning of this table for these tag ids; note that the
  768. # %security hash contains all of the keywords we've operated on in total
  769. my @kwids = keys %security;
  770. my $sql = join(',', map { $_ + 0 } @kwids);
  771. my $sth = $u->prepare("SELECT kwid, COUNT(*) FROM logtagsrecent WHERE journalid = ? AND kwid IN ($sql) GROUP BY 1");
  772. return $rollback->() if $u->err || ! $sth;
  773. $sth->execute($u->{userid});
  774. return $rollback->() if $sth->err;
  775. # now iterate over counts and find ones that are too high
  776. my %delrecent; # kwid => [ jitemid, jitemid, ... ]
  777. while (my ($kwid, $ct) = $sth->fetchrow_array) {
  778. next unless $ct > $recentlimit + 20;
  779. # get the times of the entries, the user time (lastn view uses user time), sort it, and then
  780. # we can chop off jitemids that fall below the threshold -- but only in this keyword and only clean
  781. # up some number at a time (25 at most, starting at our threshold)
  782. my $sth2 = $u->prepare(qq{
  783. SELECT t.jitemid
  784. FROM logtagsrecent t, log2 l
  785. WHERE t.journalid = l.journalid
  786. AND t.jitemid = l.jitemid
  787. AND t.journalid = ?
  788. AND t.kwid = ?
  789. ORDER BY l.eventtime DESC
  790. LIMIT $recentlimit,25
  791. });
  792. return $rollback->() if $u->err || ! $sth2;
  793. $sth2->execute($u->{userid}, $kwid);
  794. return $rollback->() if $sth2->err;
  795. # push these onto the hash for deleting below
  796. while (my $jit = $sth2->fetchrow_array) {
  797. push @{$delrecent{$kwid} ||= []}, $jit;
  798. }
  799. }
  800. # now delete any recents we need to into this format:
  801. # (kwid = 3 AND jitemid IN (2, 3, 4)) OR (kwid = ...) OR ...
  802. # but only if we have some to delete
  803. if (%delrecent) {
  804. my $del = join(' OR ', map {
  805. "(kwid = " . ($_+0) . " AND jitemid IN (" . join(',', map { $_+0 } @{$delrecent{$_}}) . "))"
  806. } keys %delrecent);
  807. $u->do("DELETE FROM logtagsrecent WHERE journalid = ? AND ($del)", undef, $u->{userid});
  808. return $rollback->() if $u->err;
  809. }
  810. # now we must get the current security values in order to come up with a proper update; note that
  811. # we select for update, which locks it so we have a consistent view of the rows
  812. $sth = $u->prepare("SELECT kwid, security, entryct FROM logkwsum WHERE journalid = ? AND kwid IN ($sql) FOR UPDATE");
  813. return $rollback->() if $u->err || ! $sth;
  814. $sth->execute($u->{userid});
  815. return $rollback->() if $sth->err;
  816. # now iterate and get the security counts
  817. my %counts;
  818. while (my ($kwid, $secu, $ct) = $sth->fetchrow_array) {
  819. $counts{$kwid}->{$secu} = $ct;
  820. }
  821. # now we want to update them, and delete any at 0
  822. my (@replace, @delete);
  823. foreach my $kwid (@kwids) {
  824. if (exists $counts{$kwid} && exists $counts{$kwid}->{$sec}) {
  825. # an old one exists
  826. my $new = $counts{$kwid}->{$sec} + $security{$kwid};
  827. if ($new > 0) {
  828. # update it
  829. push @replace, [ $kwid, $sec, $new ];
  830. } else {
  831. # delete this one
  832. push @delete, [ $kwid, $sec ];
  833. }
  834. } else {
  835. # add a new one
  836. push @replace, [ $kwid, $sec, $security{$kwid} ];
  837. }
  838. }
  839. # handle deletes in one move; well, 100 at a time
  840. while (my @list = splice(@delete, 0, 100)) {
  841. my $sql = join(' OR ', map { "(kwid = ? AND security = ?)" } 1..scalar(@list));
  842. $u->do("DELETE FROM logkwsum WHERE journalid = ? AND ($sql)",
  843. undef, $u->{userid}, map { @$_ } @list);
  844. return $rollback->() if $u->err;
  845. }
  846. # handle replaces and inserts
  847. while (my @list = splice(@replace, 0, 100)) {
  848. my $sql = join(',', map { "(?,?,?,?)" } 1..scalar(@list));
  849. $u->do("REPLACE INTO logkwsum (journalid, kwid, security, entryct) VALUES $sql",
  850. undef, map { $u->{userid}, @$_ } @list);
  851. return $rollback->() if $u->err;
  852. }
  853. # commit everything and smack caches and we're done!
  854. die $u->errstr unless $u->commit;
  855. LJ::Tags::reset_cache($u);
  856. LJ::Tags::reset_cache($u => $jitemid);
  857. return 1;
  858. }
  859. # <LJFUNC>
  860. # name: LJ::Tags::delete_logtags
  861. # class: tags
  862. # des: Deletes all tags on an entry.
  863. # args: uobj, jitemid
  864. # des-uobj: User id or object of account with entry.
  865. # des-jitemid: Journal itemid of entry to delete tags from.
  866. # returns: undef on error; 1 on success
  867. # </LJFUNC>
  868. sub delete_logtags {
  869. return undef unless LJ::is_enabled('tags');
  870. my $u = LJ::want_user(shift);
  871. my $jitemid = shift() + 0;
  872. return undef unless $u && $jitemid;
  873. # maybe this is ghetto, but it does all of the logic we would otherwise
  874. # have to duplicate here, so no sense in doing that.
  875. return LJ::Tags::update_logtags($u, $jitemid, { set_string => "", force => 1, });
  876. }
  877. # <LJFUNC>
  878. # name: LJ::Tags::reset_cache
  879. # class: tags
  880. # des: Clears out all cached information for a user's tags.
  881. # args: uobj, jitemid?
  882. # des-uobj: User id or object of account to clear cache for
  883. # des-jitemid: Either a single jitemid or an arrayref of jitemids to clear for the user. If
  884. # not present, the user's tags cache is cleared. If present, the cache for those
  885. # entries only are cleared.
  886. # returns: undef on error; 1 on success
  887. # </LJFUNC>
  888. sub reset_cache {
  889. return undef unless LJ::is_enabled('tags');
  890. while (my ($u, $jitemid) = splice(@_, 0, 2)) {
  891. next unless
  892. $u = LJ::want_user($u);
  893. # standard user tags cleanup
  894. unless ($jitemid) {
  895. delete $LJ::REQ_CACHE_USERTAGS{$u->{userid}};
  896. LJ::MemCache::delete([ $u->{userid}, "tags:$u->{userid}" ]);
  897. }
  898. # now, cleanup entries if necessary
  899. if ($jitemid) {
  900. $jitemid = [ $jitemid ]
  901. unless ref $jitemid eq 'ARRAY';
  902. LJ::MemCache::delete([ $u->{userid}, "logtag:$u->{userid}:$_" ])
  903. foreach @$jitemid;
  904. }
  905. }
  906. return 1;
  907. }
  908. # <LJFUNC>
  909. # name: LJ::Tags::create_usertag
  910. # class: tags
  911. # des: Creates tags for a user, returning the keyword ids allocated.
  912. # args: uobj, kw, opts?
  913. # des-uobj: User object to create tag on.
  914. # des-kw: Tag string (comma separated list of tags) to create.
  915. # des-opts: Optional; hashref, possible keys being 'display' and value being whether or
  916. # not this tag should be a display tag and 'parenttagid' being the tagid of a
  917. # parent tag for hierarchy. 'err_ref' optional key should be a ref to a scalar
  918. # where we will store text about errors. 'ignore_max' if set will ignore the
  919. # user's max tags limit when creating this tag.
  920. # returns: undef on error, else a hashref of { keyword => tagid } for each keyword defined
  921. # </LJFUNC>
  922. sub create_usertag {
  923. return undef unless LJ::is_enabled('tags');
  924. my $u = LJ::want_user(shift);
  925. my $kw = shift;
  926. my $opts = shift || {};
  927. return undef unless $u && $kw;
  928. # setup error stuff
  929. my $err = sub {
  930. my $fake = "";
  931. my $err_ref = $opts->{err_ref} && ref $opts->{err_ref} eq 'SCALAR' ? $opts->{err_ref} : \$fake;
  932. $$err_ref = shift() || "Unspecified error";
  933. return undef;
  934. };
  935. my $tags = [];
  936. return $err->( LJ::Lang::ml( 'taglib.error.invalid', { tagname => LJ::ehtml( $kw ) } ) )
  937. unless LJ::Tags::is_valid_tagstring($kw, $tags);
  938. # check to ensure we don't exceed the max of tags
  939. my $max = $opts->{ignore_max} ? 0 : $u->count_tags_max;
  940. if ($max && $max > 0) {
  941. my $cur = scalar(keys %{ LJ::Tags::get_usertags($u) || {} });
  942. my $tagtotal = $cur + scalar(@$tags);
  943. if ($tagtotal > $max) {
  944. return $err->(LJ::Lang::ml('taglib.error.toomany3', { max => $max,
  945. excess => $tagtotal - $max }));
  946. }
  947. }
  948. my $display = $opts->{display} ? 1 : 0;
  949. my $parentkwid = $opts->{parenttagid} ? ($opts->{parenttagid}+0) : undef;
  950. my %res;
  951. foreach my $tag (@$tags) {
  952. my $kwid = $u->get_keyword_id( $tag );
  953. return undef unless $kwid;
  954. $res{$tag} = $kwid;
  955. }
  956. my $ct = scalar keys %res;
  957. my $bind = join(',', map { "(?,?,?,?)" } 1..$ct);
  958. $u->do("INSERT IGNORE INTO usertags (journalid, kwid, parentkwid, display) VALUES $bind",
  959. undef, map { $u->{userid}, $_, $parentkwid, $display } values %res);
  960. return undef if $u->err;
  961. LJ::Tags::reset_cache($u);
  962. return \%res;
  963. }
  964. # <LJFUNC>
  965. # name: LJ::Tags::validate_tag
  966. # class: tags
  967. # des: Check the validity of a single tag.
  968. # args: tag
  969. # des-tag: The tag to check.
  970. # returns: If valid, the canonicalized tag, else, undef.
  971. # </LJFUNC>
  972. sub validate_tag {
  973. my $tag = shift;
  974. return undef unless $tag;
  975. my $list = [];
  976. return undef unless
  977. LJ::Tags::is_valid_tagstring($tag, $list);
  978. return undef if scalar(@$list) > 1;
  979. return $list->[0];
  980. }
  981. # <LJFUNC>
  982. # name: LJ::Tags::delete_usertag
  983. # class: tags
  984. # des: Deletes a tag for a user, and all mappings.
  985. # args: uobj, type, tag
  986. # des-uobj: User object to delete tag on.
  987. # des-type: Either 'id' or 'name', indicating the type of the third parameter.
  988. # des-tag: If type is 'id', this is the tag id (kwid). If type is 'name', this is the name of the
  989. # tag that we want to delete from the user.
  990. # returns: undef on error, 1 for success, 0 for tag not found
  991. # </LJFUNC>
  992. sub delete_usertag {
  993. return undef unless LJ::is_enabled('tags');
  994. my $u = LJ::want_user(shift);
  995. return undef unless $u;
  996. my ($type, $val) = @_;
  997. my $kwid;
  998. if ($type eq 'name') {
  999. my $tag = LJ::Tags::validate_tag($val);
  1000. return undef unless $tag;
  1001. $kwid = $u->get_keyword_id( $tag, 0 );
  1002. } elsif ($type eq 'id') {
  1003. $kwid = $val + 0;
  1004. }
  1005. return undef unless $kwid;
  1006. # escape sub
  1007. my $rollback = sub {
  1008. die $u->errstr unless $u->rollback;
  1009. return undef;
  1010. };
  1011. # start the big transaction
  1012. $u->begin_work;
  1013. # get items this keyword is on
  1014. my $sth = $u->prepare('SELECT jitemid FROM logtags WHERE journalid = ? AND kwid = ? FOR UPDATE');
  1015. return $rollback->() if $u->err || ! $sth;
  1016. # now get the items
  1017. $sth->execute($u->{userid}, $kwid);
  1018. return $rollback->() if $sth->err;
  1019. # now get list of jitemids for later cache clearing
  1020. my @jitemids;
  1021. push @jitemids, $_
  1022. while $_ = $sth->fetchrow_array;
  1023. # delete this tag's information from the relevant tables
  1024. foreach my $table (qw(usertags logtags logtagsrecent logkwsum)) {
  1025. # no error checking, we're just deleting data that's already semi-unlinked due
  1026. # to us already updating the userprop above
  1027. $u->do("DELETE FROM $table WHERE journalid = ? AND kwid = ?",
  1028. undef, $u->{userid}, $kwid);
  1029. }
  1030. # all done with our updates
  1031. die $u->errstr unless $u->commit;
  1032. # reset caches, have to do both of these, one for the usertags one for logtags
  1033. LJ::Tags::reset_cache($u);
  1034. LJ::Tags::reset_cache($u => \@jitemids);
  1035. return 1;
  1036. }
  1037. # <LJFUNC>
  1038. # name: LJ::Tags::rename_usertag
  1039. # class: tags
  1040. # des: Renames a tag for a user
  1041. # args: uobj, type, tag, newname, error_ref (optional)
  1042. # des-uobj: User object to delete tag on.
  1043. # des-type: Either 'id' or 'name', indicating the type of the third parameter.
  1044. # des-tag: If type is 'id', this is the tag id (kwid). If type is 'name', this is the name of the
  1045. # tag that we want to rename for the user.
  1046. # des-newname: The new name of this tag.
  1047. # des-error_ref: (optional) ref to scalar to return error messages in.
  1048. # returns: undef on error, 1 for success, 0 for tag not found
  1049. # </LJFUNC>
  1050. sub rename_usertag {
  1051. return undef unless LJ::is_enabled('tags');
  1052. my $u = LJ::want_user(shift);
  1053. return undef unless $u;
  1054. my ($type, $oldkw, $newkw, $ref) = @_;
  1055. return undef unless $type && $oldkw && $newkw;
  1056. # setup error stuff
  1057. my $err = sub {
  1058. my $fake = "";
  1059. my $err_ref = $ref && ref $ref eq 'SCALAR' ? $ref : \$fake;
  1060. $$err_ref = shift() || "Unspecified error";
  1061. return undef;
  1062. };
  1063. # validate new tag
  1064. my $newname = LJ::Tags::validate_tag($newkw);
  1065. return $err->( LJ::Lang::ml( 'taglib.error.invalid', { tagname => LJ::ehtml( $newkw ) } ) )
  1066. unless $newname;
  1067. # get a list of keyword ids to operate on
  1068. my $kwid;
  1069. if ($type eq 'name') {
  1070. my $val = LJ::Tags::validate_tag($oldkw);
  1071. return $err->( LJ::Lang::ml( 'taglib.error.invalid', { tagname => LJ::ehtml( $oldkw ) } ) )
  1072. unless $val;
  1073. $kwid = $u->get_keyword_id( $val, 0 );
  1074. } elsif ($type eq 'id') {
  1075. $kwid = $oldkw + 0;
  1076. }
  1077. return $err->() unless $kwid;
  1078. # see if this is already a keyword
  1079. my $newkwid = $u->get_keyword_id( $newname );
  1080. return undef unless $newkwid;
  1081. # see if the tag we're renaming TO already exists as a keyword,
  1082. # if so, error and suggest merging the tags
  1083. # FIXME: ask user to merge and then merge
  1084. my $tags = LJ::Tags::get_usertags( $u );
  1085. return $err->( LJ::Lang::ml( 'taglib.error.exists', { tagname => LJ::ehtml( $newname ) } ) )
  1086. if $tags->{$newkwid};
  1087. # escape sub
  1088. my $rollback = sub {
  1089. die $u->errstr unless $u->rollback;
  1090. return undef;
  1091. };
  1092. # start the big transaction
  1093. $u->begin_work;
  1094. # get items this keyword is on
  1095. my $sth = $u->prepare('SELECT jitemid FROM logtags WHERE journalid = ? AND kwid = ? FOR UPDATE');
  1096. return $rollback->() if $u->err || ! $sth;
  1097. # now get the items
  1098. $sth->execute($u->{userid}, $kwid);
  1099. return $rollback->() if $sth->err;
  1100. # now get list of jitemids for later cache clearing
  1101. my @jitemids;
  1102. push @jitemids, $_
  1103. while $_ = $sth->fetchrow_array;
  1104. # do database update to migrate from old to new
  1105. foreach my $table (qw(usertags logtags logtagsrecent logkwsum)) {
  1106. $u->do("UPDATE $table SET kwid = ? WHERE journalid = ? AND kwid = ?",
  1107. undef, $newkwid, $u->{userid}, $kwid);
  1108. return $rollback->() if $u->err;
  1109. }
  1110. # all done with our updates
  1111. die $u->errstr unless $u->commit;
  1112. # reset caches, have to do both of these, one for the usertags one for logtags
  1113. LJ::Tags::reset_cache($u);
  1114. LJ::Tags::reset_cache($u => \@jitemids);
  1115. return 1;
  1116. }
  1117. # <LJFUNC>
  1118. # name: LJ::Tags::merge_usertags
  1119. # class: tags
  1120. # des: Merges usertags
  1121. # args: uobj, newname, error_ref, oldnames
  1122. # des-uobj: User object to merge tag on.
  1123. # des-newname: new name for these tags, might be one that already exists
  1124. # des-error_ref: ref to scalar to return error messages in.
  1125. # des-oldnames: array of tags that need to be merged
  1126. # returns: undef on error, 1 for success
  1127. # </LJFUNC>
  1128. sub merge_usertags {
  1129. return undef unless LJ::is_enabled( 'tags' );
  1130. my $u = LJ::want_user( shift );
  1131. return undef unless $u;
  1132. my ( $merge_to, $ref, @merge_from ) = @_;
  1133. my $userid = $u->userid;
  1134. return undef unless $userid;
  1135. # error output
  1136. my $err = sub {
  1137. my $err_ref = $ref && ref $ref eq 'SCALAR' ? $ref : \"";
  1138. $$err_ref = shift() || "Unspecified…

Large files files are truncated, but you can click here to view the full file