PageRenderTime 68ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/cgi-bin/LJ/Tags.pm

https://bitbucket.org/anall/dw-free-work
Perl | 1616 lines | 1478 code | 52 blank | 86 comment | 61 complexity | 9b109e9a960999dc86352afbf34dd732 MD5 | raw file
Possible License(s): GPL-2.0, 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.toomany2', { max => $max,
  711. tags => LJ::ehtml( join( ", ", @to_create ) ),
  712. excess => $total - $max }));
  713. }
  714. }
  715. # now we can create the new tags, since we know we're safe
  716. # We still need to propagate ignore_max, as create_usertag does some checks of it's own.
  717. LJ::Tags::create_usertag( $u, $_, { display => 1, ignore_max => $opts->{ignore_max} } ) foreach @to_create;
  718. # %add and %delete are accurate, but we need to track necessary
  719. # security updates; this is a hash of keyword ids and a modification
  720. # value (a delta; +/-N) to be applied to that row later
  721. my %security;
  722. # get the security of this post for use in %security; do this now so
  723. # we don't interrupt the transaction below
  724. my $l2row = LJ::get_log2_row($u, $jitemid);
  725. return undef unless $l2row;
  726. # calculate security masks
  727. my $sec = LJ::Tags::get_security_level($l2row->{security}, $l2row->{allowmask});
  728. # setup a rollback bail path so that we can undo everything we've done
  729. # if anything fails in the middle; and if the rollback fails, scream loudly
  730. # and burst into flames!
  731. my $rollback = sub {
  732. die $u->errstr unless $u->rollback;
  733. return undef;
  734. };
  735. # start the big transaction, for great justice!
  736. $u->begin_work;
  737. # process additions first
  738. my @bind;
  739. foreach my $kwid (keys %add) {
  740. $security{$kwid}++;
  741. push @bind, $u->{userid}, $jitemid, $kwid;
  742. }
  743. my $recentlimit = $LJ::RECENT_TAG_LIMIT || 500;
  744. # now add all to both tables; only do $recentlimit rows ($recentlimit * 3 bind vars) at a time
  745. while (my @list = splice(@bind, 0, 3 * $recentlimit)) {
  746. my $sql = join(',', map { "(?,?,?)" } 1..(scalar(@list)/3));
  747. $u->do("REPLACE INTO logtags (journalid, jitemid, kwid) VALUES $sql", undef, @list);
  748. return $rollback->() if $u->err;
  749. $u->do("REPLACE INTO logtagsrecent (journalid, jitemid, kwid) VALUES $sql", undef, @list);
  750. return $rollback->() if $u->err;
  751. }
  752. # now process deletions
  753. @bind = ();
  754. foreach my $kwid (keys %delete) {
  755. $security{$kwid}--;
  756. push @bind, $kwid;
  757. }
  758. # now run the SQL
  759. while (my @list = splice(@bind, 0, $recentlimit)) {
  760. my $sql = join(',', map { $_ + 0 } @list);
  761. $u->do("DELETE FROM logtags WHERE journalid = ? AND jitemid = ? AND kwid IN ($sql)",
  762. undef, $u->{userid}, $jitemid);
  763. return $rollback->() if $u->err;
  764. $u->do("DELETE FROM logtagsrecent WHERE journalid = ? AND kwid IN ($sql) AND jitemid = ?",
  765. undef, $u->{userid}, $jitemid);
  766. return $rollback->() if $u->err;
  767. }
  768. # now handle lazy cleaning of this table for these tag ids; note that the
  769. # %security hash contains all of the keywords we've operated on in total
  770. my @kwids = keys %security;
  771. my $sql = join(',', map { $_ + 0 } @kwids);
  772. my $sth = $u->prepare("SELECT kwid, COUNT(*) FROM logtagsrecent WHERE journalid = ? AND kwid IN ($sql) GROUP BY 1");
  773. return $rollback->() if $u->err || ! $sth;
  774. $sth->execute($u->{userid});
  775. return $rollback->() if $sth->err;
  776. # now iterate over counts and find ones that are too high
  777. my %delrecent; # kwid => [ jitemid, jitemid, ... ]
  778. while (my ($kwid, $ct) = $sth->fetchrow_array) {
  779. next unless $ct > $recentlimit + 20;
  780. # get the times of the entries, the user time (lastn view uses user time), sort it, and then
  781. # we can chop off jitemids that fall below the threshold -- but only in this keyword and only clean
  782. # up some number at a time (25 at most, starting at our threshold)
  783. my $sth2 = $u->prepare(qq{
  784. SELECT t.jitemid
  785. FROM logtagsrecent t, log2 l
  786. WHERE t.journalid = l.journalid
  787. AND t.jitemid = l.jitemid
  788. AND t.journalid = ?
  789. AND t.kwid = ?
  790. ORDER BY l.eventtime DESC
  791. LIMIT $recentlimit,25
  792. });
  793. return $rollback->() if $u->err || ! $sth2;
  794. $sth2->execute($u->{userid}, $kwid);
  795. return $rollback->() if $sth2->err;
  796. # push these onto the hash for deleting below
  797. while (my $jit = $sth2->fetchrow_array) {
  798. push @{$delrecent{$kwid} ||= []}, $jit;
  799. }
  800. }
  801. # now delete any recents we need to into this format:
  802. # (kwid = 3 AND jitemid IN (2, 3, 4)) OR (kwid = ...) OR ...
  803. # but only if we have some to delete
  804. if (%delrecent) {
  805. my $del = join(' OR ', map {
  806. "(kwid = " . ($_+0) . " AND jitemid IN (" . join(',', map { $_+0 } @{$delrecent{$_}}) . "))"
  807. } keys %delrecent);
  808. $u->do("DELETE FROM logtagsrecent WHERE journalid = ? AND ($del)", undef, $u->{userid});
  809. return $rollback->() if $u->err;
  810. }
  811. # now we must get the current security values in order to come up with a proper update; note that
  812. # we select for update, which locks it so we have a consistent view of the rows
  813. $sth = $u->prepare("SELECT kwid, security, entryct FROM logkwsum WHERE journalid = ? AND kwid IN ($sql) FOR UPDATE");
  814. return $rollback->() if $u->err || ! $sth;
  815. $sth->execute($u->{userid});
  816. return $rollback->() if $sth->err;
  817. # now iterate and get the security counts
  818. my %counts;
  819. while (my ($kwid, $secu, $ct) = $sth->fetchrow_array) {
  820. $counts{$kwid}->{$secu} = $ct;
  821. }
  822. # now we want to update them, and delete any at 0
  823. my (@replace, @delete);
  824. foreach my $kwid (@kwids) {
  825. if (exists $counts{$kwid} && exists $counts{$kwid}->{$sec}) {
  826. # an old one exists
  827. my $new = $counts{$kwid}->{$sec} + $security{$kwid};
  828. if ($new > 0) {
  829. # update it
  830. push @replace, [ $kwid, $sec, $new ];
  831. } else {
  832. # delete this one
  833. push @delete, [ $kwid, $sec ];
  834. }
  835. } else {
  836. # add a new one
  837. push @replace, [ $kwid, $sec, $security{$kwid} ];
  838. }
  839. }
  840. # handle deletes in one move; well, 100 at a time
  841. while (my @list = splice(@delete, 0, 100)) {
  842. my $sql = join(' OR ', map { "(kwid = ? AND security = ?)" } 1..scalar(@list));
  843. $u->do("DELETE FROM logkwsum WHERE journalid = ? AND ($sql)",
  844. undef, $u->{userid}, map { @$_ } @list);
  845. return $rollback->() if $u->err;
  846. }
  847. # handle replaces and inserts
  848. while (my @list = splice(@replace, 0, 100)) {
  849. my $sql = join(',', map { "(?,?,?,?)" } 1..scalar(@list));
  850. $u->do("REPLACE INTO logkwsum (journalid, kwid, security, entryct) VALUES $sql",
  851. undef, map { $u->{userid}, @$_ } @list);
  852. return $rollback->() if $u->err;
  853. }
  854. # commit everything and smack caches and we're done!
  855. die $u->errstr unless $u->commit;
  856. LJ::Tags::reset_cache($u);
  857. LJ::Tags::reset_cache($u => $jitemid);
  858. return 1;
  859. }
  860. # <LJFUNC>
  861. # name: LJ::Tags::delete_logtags
  862. # class: tags
  863. # des: Deletes all tags on an entry.
  864. # args: uobj, jitemid
  865. # des-uobj: User id or object of account with entry.
  866. # des-jitemid: Journal itemid of entry to delete tags from.
  867. # returns: undef on error; 1 on success
  868. # </LJFUNC>
  869. sub delete_logtags {
  870. return undef unless LJ::is_enabled('tags');
  871. my $u = LJ::want_user(shift);
  872. my $jitemid = shift() + 0;
  873. return undef unless $u && $jitemid;
  874. # maybe this is ghetto, but it does all of the logic we would otherwise
  875. # have to duplicate here, so no sense in doing that.
  876. return LJ::Tags::update_logtags($u, $jitemid, { set_string => "", force => 1, });
  877. }
  878. # <LJFUNC>
  879. # name: LJ::Tags::reset_cache
  880. # class: tags
  881. # des: Clears out all cached information for a user's tags.
  882. # args: uobj, jitemid?
  883. # des-uobj: User id or object of account to clear cache for
  884. # des-jitemid: Either a single jitemid or an arrayref of jitemids to clear for the user. If
  885. # not present, the user's tags cache is cleared. If present, the cache for those
  886. # entries only are cleared.
  887. # returns: undef on error; 1 on success
  888. # </LJFUNC>
  889. sub reset_cache {
  890. return undef unless LJ::is_enabled('tags');
  891. while (my ($u, $jitemid) = splice(@_, 0, 2)) {
  892. next unless
  893. $u = LJ::want_user($u);
  894. # standard user tags cleanup
  895. unless ($jitemid) {
  896. delete $LJ::REQ_CACHE_USERTAGS{$u->{userid}};
  897. LJ::MemCache::delete([ $u->{userid}, "tags:$u->{userid}" ]);
  898. }
  899. # now, cleanup entries if necessary
  900. if ($jitemid) {
  901. $jitemid = [ $jitemid ]
  902. unless ref $jitemid eq 'ARRAY';
  903. LJ::MemCache::delete([ $u->{userid}, "logtag:$u->{userid}:$_" ])
  904. foreach @$jitemid;
  905. }
  906. }
  907. return 1;
  908. }
  909. # <LJFUNC>
  910. # name: LJ::Tags::create_usertag
  911. # class: tags
  912. # des: Creates tags for a user, returning the keyword ids allocated.
  913. # args: uobj, kw, opts?
  914. # des-uobj: User object to create tag on.
  915. # des-kw: Tag string (comma separated list of tags) to create.
  916. # des-opts: Optional; hashref, possible keys being 'display' and value being whether or
  917. # not this tag should be a display tag and 'parenttagid' being the tagid of a
  918. # parent tag for hierarchy. 'err_ref' optional key should be a ref to a scalar
  919. # where we will store text about errors. 'ignore_max' if set will ignore the
  920. # user's max tags limit when creating this tag.
  921. # returns: undef on error, else a hashref of { keyword => tagid } for each keyword defined
  922. # </LJFUNC>
  923. sub create_usertag {
  924. return undef unless LJ::is_enabled('tags');
  925. my $u = LJ::want_user(shift);
  926. my $kw = shift;
  927. my $opts = shift || {};
  928. return undef unless $u && $kw;
  929. # setup error stuff
  930. my $err = sub {
  931. my $fake = "";
  932. my $err_ref = $opts->{err_ref} && ref $opts->{err_ref} eq 'SCALAR' ? $opts->{err_ref} : \$fake;
  933. $$err_ref = shift() || "Unspecified error";
  934. return undef;
  935. };
  936. my $tags = [];
  937. return $err->( LJ::Lang::ml( 'taglib.error.invalid', { tagname => LJ::ehtml( $kw ) } ) )
  938. unless LJ::Tags::is_valid_tagstring($kw, $tags);
  939. # check to ensure we don't exceed the max of tags
  940. my $max = $opts->{ignore_max} ? 0 : $u->count_tags_max;
  941. if ($max && $max > 0) {
  942. my $cur = scalar(keys %{ LJ::Tags::get_usertags($u) || {} });
  943. my $tagtotal = $cur + scalar(@$tags);
  944. if ($tagtotal > $max) {
  945. return $err->(LJ::Lang::ml('taglib.error.toomany2', { max => $max,
  946. tags => LJ::ehtml( join( ", ", @$tags ) ) ,
  947. excess => $tagtotal - $max }));
  948. }
  949. }
  950. my $display = $opts->{display} ? 1 : 0;
  951. my $parentkwid = $opts->{parenttagid} ? ($opts->{parenttagid}+0) : undef;
  952. my %res;
  953. foreach my $tag (@$tags) {
  954. my $kwid = $u->get_keyword_id( $tag );
  955. return undef unless $kwid;
  956. $res{$tag} = $kwid;
  957. }
  958. my $ct = scalar keys %res;
  959. my $bind = join(',', map { "(?,?,?,?)" } 1..$ct);
  960. $u->do("INSERT IGNORE INTO usertags (journalid, kwid, parentkwid, display) VALUES $bind",
  961. undef, map { $u->{userid}, $_, $parentkwid, $display } values %res);
  962. return undef if $u->err;
  963. LJ::Tags::reset_cache($u);
  964. return \%res;
  965. }
  966. # <LJFUNC>
  967. # name: LJ::Tags::validate_tag
  968. # class: tags
  969. # des: Check the validity of a single tag.
  970. # args: tag
  971. # des-tag: The tag to check.
  972. # returns: If valid, the canonicalized tag, else, undef.
  973. # </LJFUNC>
  974. sub validate_tag {
  975. my $tag = shift;
  976. return undef unless $tag;
  977. my $list = [];
  978. return undef unless
  979. LJ::Tags::is_valid_tagstring($tag, $list);
  980. return undef if scalar(@$list) > 1;
  981. return $list->[0];
  982. }
  983. # <LJFUNC>
  984. # name: LJ::Tags::delete_usertag
  985. # class: tags
  986. # des: Deletes a tag for a user, and all mappings.
  987. # args: uobj, type, tag
  988. # des-uobj: User object to delete tag on.
  989. # des-type: Either 'id' or 'name', indicating the type of the third parameter.
  990. # des-tag: If type is 'id', this is the tag id (kwid). If type is 'name', this is the name of the
  991. # tag that we want to delete from the user.
  992. # returns: undef on error, 1 for success, 0 for tag not found
  993. # </LJFUNC>
  994. sub delete_usertag {
  995. return undef unless LJ::is_enabled('tags');
  996. my $u = LJ::want_user(shift);
  997. return undef unless $u;
  998. my ($type, $val) = @_;
  999. my $kwid;
  1000. if ($type eq 'name') {
  1001. my $tag = LJ::Tags::validate_tag($val);
  1002. return undef unless $tag;
  1003. $kwid = $u->get_keyword_id( $tag, 0 );
  1004. } elsif ($type eq 'id') {
  1005. $kwid = $val + 0;
  1006. }
  1007. return undef unless $kwid;
  1008. # escape sub
  1009. my $rollback = sub {
  1010. die $u->errstr unless $u->rollback;
  1011. return undef;
  1012. };
  1013. # start the big transaction
  1014. $u->begin_work;
  1015. # get items this keyword is on
  1016. my $sth = $u->prepare('SELECT jitemid FROM logtags WHERE journalid = ? AND kwid = ? FOR UPDATE');
  1017. return $rollback->() if $u->err || ! $sth;
  1018. # now get the items
  1019. $sth->execute($u->{userid}, $kwid);
  1020. return $rollback->() if $sth->err;
  1021. # now get list of jitemids for later cache clearing
  1022. my @jitemids;
  1023. push @jitemids, $_
  1024. while $_ = $sth->fetchrow_array;
  1025. # delete this tag's information from the relevant tables
  1026. foreach my $table (qw(usertags logtags logtagsrecent logkwsum)) {
  1027. # no error checking, we're just deleting data that's already semi-unlinked due
  1028. # to us already updating the userprop above
  1029. $u->do("DELETE FROM $table WHERE journalid = ? AND kwid = ?",
  1030. undef, $u->{userid}, $kwid);
  1031. }
  1032. # all done with our updates
  1033. die $u->errstr unless $u->commit;
  1034. # reset caches, have to do both of these, one for the usertags one for logtags
  1035. LJ::Tags::reset_cache($u);
  1036. LJ::Tags::reset_cache($u => \@jitemids);
  1037. return 1;
  1038. }
  1039. # <LJFUNC>
  1040. # name: LJ::Tags::rename_usertag
  1041. # class: tags
  1042. # des: Renames a tag for a user
  1043. # args: uobj, type, tag, newname, error_ref (optional)
  1044. # des-uobj: User object to delete tag on.
  1045. # des-type: Either 'id' or 'name', indicating the type of the third parameter.
  1046. # des-tag: If type is 'id', this is the tag id (kwid). If type is 'name', this is the name of the
  1047. # tag that we want to rename for the user.
  1048. # des-newname: The new name of this tag.
  1049. # des-error_ref: (optional) ref to scalar to return error messages in.
  1050. # returns: undef on error, 1 for success, 0 for tag not found
  1051. # </LJFUNC>
  1052. sub rename_usertag {
  1053. return undef unless LJ::is_enabled('tags');
  1054. my $u = LJ::want_user(shift);
  1055. return undef unless $u;
  1056. my ($type, $oldkw, $newkw, $ref) = @_;
  1057. return undef unless $type && $oldkw && $newkw;
  1058. # setup error stuff
  1059. my $err = sub {
  1060. my $fake = "";
  1061. my $err_ref = $ref && ref $ref eq 'SCALAR' ? $ref : \$fake;
  1062. $$err_ref = shift() || "Unspecified error";
  1063. return undef;
  1064. };
  1065. # validate new tag
  1066. my $newname = LJ::Tags::validate_tag($newkw);
  1067. return $err->( LJ::Lang::ml( 'taglib.error.invalid', { tagname => LJ::ehtml( $newkw ) } ) )
  1068. unless $newname;
  1069. # get a list of keyword ids to operate on
  1070. my $kwid;
  1071. if ($type eq 'name') {
  1072. my $val = LJ::Tags::validate_tag($oldkw);
  1073. return $err->( LJ::Lang::ml( 'taglib.error.invalid', { tagname => LJ::ehtml( $oldkw ) } ) )
  1074. unless $val;
  1075. $kwid = $u->get_keyword_id( $val, 0 );
  1076. } elsif ($type eq 'id') {
  1077. $kwid = $oldkw + 0;
  1078. }
  1079. return $err->() unless $kwid;
  1080. # see if this is already a keyword
  1081. my $newkwid = $u->get_keyword_id( $newname );
  1082. return undef unless $newkwid;
  1083. # see if the tag we're renaming TO already exists as a keyword,
  1084. # if so, error and suggest merging the tags
  1085. # FIXME: ask user to merge and then merge
  1086. my $tags = LJ::Tags::get_usertags( $u );
  1087. return $err->( LJ::Lang::ml( 'taglib.error.exists', { tagname => LJ::ehtml( $newname ) } ) )
  1088. if $tags->{$newkwid};
  1089. # escape sub
  1090. my $rollback = sub {
  1091. die $u->errstr unless $u->rollback;
  1092. return undef;
  1093. };
  1094. # start the big transaction
  1095. $u->begin_work;
  1096. # get items this keyword is on
  1097. my $sth = $u->prepare('SELECT jitemid FROM logtags WHERE journalid = ? AND kwid = ? FOR UPDATE');
  1098. return $rollback->() if $u->err || ! $sth;
  1099. # now get the items
  1100. $sth->execute($u->{userid}, $kwid);
  1101. return $rollback->() if $sth->err;
  1102. # now get list of jitemids for later cache clearing
  1103. my @jitemids;
  1104. push @jitemids, $_
  1105. while $_ = $sth->fetchrow_array;
  1106. # do database update to migrate from old to new
  1107. foreach my $table (qw(usertags logtags logtagsrecent logkwsum)) {
  1108. $u->do("UPDATE $table SET kwid = ? WHERE journalid = ? AND kwid = ?",
  1109. undef, $newkwid, $u->{userid}, $kwid);
  1110. return $rollback->() if $u->err;
  1111. }
  1112. # all done with our updates
  1113. die $u->errstr unless $u->commit;
  1114. # reset caches, have to do both of these, one for the usertags one for logtags
  1115. LJ::Tags::reset_cache($u);
  1116. LJ::Tags::reset_cache($u => \@jitemids);
  1117. return 1;
  1118. }
  1119. # <LJFUNC>
  1120. # name: LJ::Tags::merge_usertags
  1121. # class: tags
  1122. # des: Merges usertags
  1123. # args: uobj, newname, error_ref, oldnames
  1124. # des-uobj: User object to merge tag on.
  1125. # des-newname: new name for these tags, might be one that already exists
  1126. # des-error_ref: ref to scalar to return error messages in.
  1127. # des-oldnames: array of tags that need to be merged
  1128. # returns: undef on error, 1 for success
  1129. # </LJFUNC>
  1130. sub merge_usertags {
  1131. return undef unless LJ::is_enabled( 'tags' );
  1132. my $u = LJ::want_user( shift );
  1133. return undef unless $u;
  1134. my ( $merge_to, $ref, @merge_fr

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