PageRenderTime 74ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/src/models/m_rsc_update.erl

http://github.com/zotonic/zotonic
Erlang | 1343 lines | 1108 code | 153 blank | 82 comment | 26 complexity | 0d661c012857703b4c44dc91dd2b8585 MD5 | raw file
Possible License(s): Apache-2.0, CC-BY-SA-4.0, MIT, LGPL-2.1, BSD-3-Clause
  1. %% @author Marc Worrell <marc@worrell.nl>
  2. %% @copyright 2009-2015 Marc Worrell, Arjan Scherpenisse
  3. %% @doc Update routines for resources. For use by the m_rsc module.
  4. %% Copyright 2009-2015 Marc Worrell, Arjan Scherpenisse
  5. %%
  6. %% Licensed under the Apache License, Version 2.0 (the "License");
  7. %% you may not use this file except in compliance with the License.
  8. %% You may obtain a copy of the License at
  9. %%
  10. %% http://www.apache.org/licenses/LICENSE-2.0
  11. %%
  12. %% Unless required by applicable law or agreed to in writing, software
  13. %% distributed under the License is distributed on an "AS IS" BASIS,
  14. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. %% See the License for the specific language governing permissions and
  16. %% limitations under the License.
  17. -module(m_rsc_update).
  18. -author("Marc Worrell <marc@worrell.nl").
  19. %% interface functions
  20. -export([
  21. insert/2,
  22. insert/3,
  23. delete/2,
  24. update/3,
  25. update/4,
  26. duplicate/3,
  27. merge_delete/3,
  28. flush/2,
  29. normalize_props/3,
  30. normalize_props/4,
  31. delete_nocheck/2,
  32. props_filter/3,
  33. test/0
  34. ]).
  35. -include_lib("zotonic.hrl").
  36. -record(rscupd, {
  37. id=undefined,
  38. is_escape_texts=true,
  39. is_acl_check=true,
  40. is_import=false,
  41. is_no_touch=false,
  42. expected=[]
  43. }).
  44. %% @doc Insert a new resource. Crashes when insertion is not allowed.
  45. -spec insert(list(), #context{}) -> {ok, integer()}.
  46. insert(Props, Context) ->
  47. insert(Props, [{escape_texts, true}], Context).
  48. -spec insert(list(), list()|boolean(), #context{}) -> {ok, integer()}.
  49. insert(Props, Options, Context) ->
  50. PropsDefaults = props_defaults(Props, Context),
  51. update(insert_rsc, PropsDefaults, Options, Context).
  52. %% @doc Delete a resource
  53. -spec delete(integer(), #context{}) -> ok.
  54. delete(Id, Context) when is_integer(Id), Id /= 1 ->
  55. case z_acl:rsc_deletable(Id, Context) of
  56. true ->
  57. case m_rsc:is_a(Id, category, Context) of
  58. true ->
  59. m_category:delete(Id, undefined, Context);
  60. false ->
  61. delete_nocheck(Id, Context)
  62. end;
  63. false ->
  64. throw({error, eacces})
  65. end.
  66. %% @doc Delete a resource, no check on rights etc is made. This is called by m_category:delete/3
  67. %% @throws {error, Reason}
  68. -spec delete_nocheck(integer(), #context{}) -> ok.
  69. delete_nocheck(Id, Context) ->
  70. delete_nocheck(Id, undefined, Context).
  71. delete_nocheck(Id, OptFollowUpId, Context) ->
  72. Referrers = m_edge:subjects(Id, Context),
  73. CatList = m_rsc:is_a(Id, Context),
  74. Props = m_rsc:get(Id, Context),
  75. F = fun(Ctx) ->
  76. z_notifier:notify_sync(#rsc_delete{id=Id, is_a=CatList}, Ctx),
  77. m_rsc_gone:gone(Id, OptFollowUpId, Ctx),
  78. z_db:delete(rsc, Id, Ctx)
  79. end,
  80. {ok, _RowsDeleted} = z_db:transaction(F, Context),
  81. % Sync the caches
  82. [ z_depcache:flush(SubjectId, Context) || SubjectId <- Referrers ],
  83. flush(Id, CatList, Context),
  84. %% Notify all modules that the rsc has been deleted
  85. z_notifier:notify_sync(
  86. #rsc_update_done{
  87. action=delete,
  88. id=Id,
  89. pre_is_a=CatList,
  90. post_is_a=[],
  91. pre_props=Props,
  92. post_props=[]
  93. }, Context),
  94. z_edge_log_server:check(Context),
  95. ok.
  96. %% @doc Merge two resources, delete the losing resource.
  97. -spec merge_delete(integer(), integer(), #context{}) -> ok | {error, term()}.
  98. merge_delete(WinnerId, WinnerId, _Context) ->
  99. ok;
  100. merge_delete(_WinnerId, 1, _Context) ->
  101. throw({error, eacces});
  102. merge_delete(WinnerId, LoserId, Context) when is_integer(WinnerId), is_integer(LoserId) ->
  103. case z_acl:rsc_deletable(LoserId, Context)
  104. andalso z_acl:rsc_editable(WinnerId, Context)
  105. of
  106. true ->
  107. case m_rsc:is_a(WinnerId, category, Context) of
  108. true ->
  109. m_category:delete(LoserId, WinnerId, Context);
  110. false ->
  111. merge_delete_nocheck(WinnerId, LoserId, Context)
  112. end;
  113. false ->
  114. throw({error, eacces})
  115. end.
  116. %% @doc Merge two resources, delete the 'loser'
  117. -spec merge_delete_nocheck(integer(), integer(), #context{}) -> ok.
  118. merge_delete_nocheck(WinnerId, LoserId, Context) ->
  119. z_notifier:map(#rsc_merge{winner_id=WinnerId, looser_id=LoserId}, Context),
  120. ok = m_edge:merge(WinnerId, LoserId, Context),
  121. m_media:merge(WinnerId, LoserId, Context),
  122. m_identity:merge(WinnerId, LoserId, Context),
  123. move_creator_modifier_ids(WinnerId, LoserId, Context),
  124. PropsLooser = m_rsc:get(LoserId, Context),
  125. ok = delete_nocheck(LoserId, WinnerId, Context),
  126. case merge_copy_props(WinnerId, PropsLooser, Context) of
  127. [] ->
  128. ok;
  129. UpdProps ->
  130. {ok, _} = update(WinnerId, UpdProps, [{escape_texts, false}], Context)
  131. end,
  132. ok.
  133. move_creator_modifier_ids(WinnerId, LoserId, Context) ->
  134. Ids = z_db:q("select id
  135. from rsc
  136. where (creator_id = $1 or modifier_id = $1)
  137. and id <> $1",
  138. [LoserId],
  139. Context),
  140. z_db:q("update rsc set creator_id = $1 where creator_id = $2",
  141. [WinnerId, LoserId],
  142. Context,
  143. 1200000),
  144. z_db:q("update rsc set modifier_id = $1 where modifier_id = $2",
  145. [WinnerId, LoserId],
  146. Context,
  147. 1200000),
  148. lists:foreach(
  149. fun(Id) ->
  150. flush(Id, [], Context)
  151. end,
  152. Ids).
  153. merge_copy_props(WinnerId, Props, Context) ->
  154. merge_copy_props(WinnerId, Props, [], Context).
  155. merge_copy_props(_WinnerId, [], Acc, _Context) ->
  156. lists:reverse(Acc);
  157. merge_copy_props(WinnerId, [{P,_}|Ps], Acc, Context)
  158. when P =:= creator; P =:= creator_id; P =:= modifier; P =:= modifier_id;
  159. P =:= created; P =:= modified; P =:= version;
  160. P =:= id; P =:= is_published; P =:= is_protected; P =:= is_dependent;
  161. P =:= is_authoritative; P =:= pivot_geocode; P =:= pivot_geocode_qhash;
  162. P =:= category_id ->
  163. merge_copy_props(WinnerId, Ps, Acc, Context);
  164. merge_copy_props(WinnerId, [{_,Empty}|Ps], Acc, Context)
  165. when Empty =:= []; Empty =:= <<>>; Empty =:= undefined ->
  166. merge_copy_props(WinnerId, Ps, Acc, Context);
  167. merge_copy_props(WinnerId, [{P,_} = PV|Ps], Acc, Context) ->
  168. case m_rsc:p_no_acl(WinnerId, P, Context) of
  169. Empty when Empty =:= []; Empty =:= <<>>; Empty =:= undefined ->
  170. merge_copy_props(WinnerId, Ps, [PV|Acc], Context);
  171. _Value ->
  172. merge_copy_props(WinnerId, Ps, Acc, Context)
  173. end.
  174. %% Flush all cached entries depending on this entry, one of its subjects or its categories.
  175. flush(Id, Context) ->
  176. CatList = m_rsc:is_a(Id, Context),
  177. flush(Id, CatList, Context).
  178. flush(Id, CatList, Context) ->
  179. z_depcache:flush(Id, Context),
  180. [ z_depcache:flush(Cat, Context) || Cat <- CatList ],
  181. ok.
  182. %% @doc Duplicate a resource, creating a new resource with the given title.
  183. %% @throws {error, Reason}
  184. -spec duplicate(integer(), list(), #context{}) -> {ok, integer()}.
  185. duplicate(Id, DupProps, Context) ->
  186. case z_acl:rsc_visible(Id, Context) of
  187. true ->
  188. Props = m_rsc:get_raw(Id, Context),
  189. FilteredProps = props_filter_protected(Props, #rscupd{id=insert_rsc, is_escape_texts=false}),
  190. SafeDupProps = z_sanitize:escape_props(DupProps, Context),
  191. InsProps = lists:foldl(
  192. fun({Key, Value}, Acc) ->
  193. z_utils:prop_replace(Key, Value, Acc)
  194. end,
  195. FilteredProps,
  196. SafeDupProps ++ [
  197. {name,undefined}, {uri,undefined}, {page_path,undefined},
  198. {is_authoritative,true}, {is_protected,false},
  199. {slug,undefined}
  200. ]),
  201. {ok, NewId} = insert(InsProps, false, Context),
  202. m_edge:duplicate(Id, NewId, Context),
  203. m_media:duplicate(Id, NewId, Context),
  204. {ok, NewId};
  205. false ->
  206. throw({error, eacces})
  207. end.
  208. %% @doc Update a resource
  209. %% @spec update(Id, Props, Context) -> {ok, Id}
  210. %% @throws {error, Reason}
  211. -spec update(integer()|insert_rsc, list(), #context{}) -> {ok, integer()} | {error, term()}.
  212. update(Id, Props, Context) ->
  213. update(Id, Props, [], Context).
  214. -spec update(integer()|insert_rsc, list(), list()|boolean(), #context{}) -> {ok, integer()} | {error, term()}.
  215. update(Id, Props, false, Context) ->
  216. update(Id, Props, [{escape_texts, false}], Context);
  217. update(Id, Props, true, Context) ->
  218. update(Id, Props, [{escape_texts, true}], Context);
  219. %% @doc Resource updater function
  220. %% [Options]: {escape_texts, true|false (default: true}, {acl_check: true|false (default: true)}
  221. %% {escape_texts, false} checks if the texts are escaped, and if not then it will escape. This prevents "double-escaping" of texts.
  222. update(Id, Props, Options, Context) when is_integer(Id) orelse Id =:= insert_rsc ->
  223. RscUpd = #rscupd{
  224. id = Id,
  225. is_escape_texts = proplists:get_value(escape_texts, Options, true),
  226. is_acl_check = proplists:get_value(acl_check, Options, true),
  227. is_import = proplists:get_value(is_import, Options, false),
  228. is_no_touch = proplists:get_value(no_touch, Options, false)
  229. andalso z_acl:is_admin(Context),
  230. expected = proplists:get_value(expected, Options, [])
  231. },
  232. update_imported_check(RscUpd, Props, Context).
  233. update_imported_check(#rscupd{is_import=true, id=Id} = RscUpd, Props, Context) when is_integer(Id) ->
  234. case m_rsc:exists(Id, Context) of
  235. false ->
  236. {ok, CatId} = m_category:name_to_id(other, Context),
  237. 1 = z_db:q("insert into rsc (id, creator_id, is_published, category_id)
  238. values ($1, $2, false, $3)",
  239. [Id, z_acl:user(Context), CatId],
  240. Context);
  241. true ->
  242. ok
  243. end,
  244. update_editable_check(RscUpd, Props, Context);
  245. update_imported_check(RscUpd, Props, Context) ->
  246. update_editable_check(RscUpd, Props, Context).
  247. update_editable_check(#rscupd{id=Id, is_acl_check=true} = RscUpd, Props, Context) when is_integer(Id) ->
  248. case z_acl:rsc_editable(Id, Context) of
  249. true ->
  250. update_normalize_props(RscUpd, Props, Context);
  251. false ->
  252. E = case m_rsc:p(Id, is_authoritative, Context) of
  253. false -> {error, non_authoritative};
  254. true -> {error, eacces}
  255. end,
  256. throw(E)
  257. end;
  258. update_editable_check(RscUpd, Props, Context) ->
  259. update_normalize_props(RscUpd, Props, Context).
  260. update_normalize_props(#rscupd{id=Id} = RscUpd, Props, Context) when is_list(Props) ->
  261. AtomProps = normalize_props(Id, Props, Context),
  262. update_transaction(RscUpd, fun(_, _, _) -> {ok, AtomProps} end, Context);
  263. update_normalize_props(RscUpd, Func, Context) when is_function(Func) ->
  264. update_transaction(RscUpd, Func, Context).
  265. update_transaction(RscUpd, Func, Context) ->
  266. Result = z_db:transaction(
  267. fun (Ctx) ->
  268. update_transaction_fun_props(RscUpd, Func, Ctx)
  269. end,
  270. Context),
  271. update_result(Result, RscUpd, Context).
  272. update_result({ok, NewId, notchanged}, _RscUpd, _Context) ->
  273. {ok, NewId};
  274. update_result({ok, NewId, OldProps, NewProps, OldCatList, IsCatInsert}, #rscupd{id=Id}, Context) ->
  275. % Flush some low level caches
  276. case proplists:get_value(name, NewProps) of
  277. undefined -> nop;
  278. Name -> z_depcache:flush({rsc_name, z_string:to_name(Name)}, Context)
  279. end,
  280. case proplists:get_value(uri, NewProps) of
  281. undefined -> nop;
  282. Uri -> z_depcache:flush({rsc_uri, z_convert:to_list(Uri)}, Context)
  283. end,
  284. % Flush category caches if a category is inserted.
  285. case IsCatInsert of
  286. true -> m_category:flush(Context);
  287. false -> nop
  288. end,
  289. % Flush all cached content that is depending on one of the updated categories
  290. z_depcache:flush(NewId, Context),
  291. NewCatList = m_rsc:is_a(NewId, Context),
  292. Cats = lists:usort(NewCatList ++ OldCatList),
  293. [ z_depcache:flush(Cat, Context) || Cat <- Cats ],
  294. % Notify that a new resource has been inserted, or that an existing one is updated
  295. Note = #rsc_update_done{
  296. action= case Id of insert_rsc -> insert; _ -> update end,
  297. id=NewId,
  298. pre_is_a=OldCatList,
  299. post_is_a=NewCatList,
  300. pre_props=OldProps,
  301. post_props=NewProps
  302. },
  303. z_notifier:notify_sync(Note, Context),
  304. % Return the updated or inserted id
  305. {ok, NewId};
  306. update_result({rollback, {_Why, _} = Er}, _RscUpd, _Context) ->
  307. throw(Er);
  308. update_result({error, _} = Error, _RscUpd, _Context) ->
  309. throw(Error).
  310. %% @doc This is running inside the rsc update db transaction
  311. update_transaction_fun_props(#rscupd{id=Id} = RscUpd, Func, Context) ->
  312. Raw = get_raw_lock(Id, Context),
  313. case Func(Id, Raw, Context) of
  314. {ok, UpdateProps} ->
  315. EditableProps = props_filter_protected(
  316. props_filter(
  317. props_trim(UpdateProps), [], Context),
  318. RscUpd),
  319. AclCheckedProps = case z_acl:rsc_update_check(Id, EditableProps, Context) of
  320. L when is_list(L) -> L;
  321. {error, Reason} -> throw({error, Reason})
  322. end,
  323. AutogeneratedProps = props_autogenerate(Id, AclCheckedProps, Context),
  324. SafeProps = case RscUpd#rscupd.is_escape_texts of
  325. true -> z_sanitize:escape_props(AutogeneratedProps, Context);
  326. false -> z_sanitize:escape_props_check(AutogeneratedProps, Context)
  327. end,
  328. ok = preflight_check(Id, SafeProps, Context),
  329. throw_if_category_not_allowed(Id, SafeProps, RscUpd#rscupd.is_acl_check, Context),
  330. update_transaction_fun_insert(RscUpd, SafeProps, Raw, UpdateProps, Context);
  331. {error, _} = Error ->
  332. {rollback, Error}
  333. end.
  334. get_raw_lock(insert_rsc, _Context) -> [];
  335. get_raw_lock(Id, Context) -> m_rsc:get_raw_lock(Id, Context).
  336. update_transaction_fun_insert(#rscupd{id=insert_rsc} = RscUpd, Props, _Raw, UpdateProps, Context) ->
  337. % Allow the initial insertion props to be modified.
  338. CategoryId = z_convert:to_integer(proplists:get_value(category_id, Props)),
  339. InsProps = z_notifier:foldr(#rsc_insert{}, [{category_id, CategoryId}, {version, 0}], Context),
  340. % Check if the user is allowed to create the resource
  341. InsertId = case proplists:get_value(creator_id, UpdateProps) of
  342. self ->
  343. {ok, InsId} = z_db:insert(rsc, [{creator_id, undefined} | InsProps], Context),
  344. 1 = z_db:q("update rsc set creator_id = id where id = $1", [InsId], Context),
  345. InsId;
  346. CreatorId when is_integer(CreatorId) ->
  347. {ok, InsId} = z_db:insert(rsc, [{creator_id, CreatorId} | InsProps], Context),
  348. InsId;
  349. undefined ->
  350. {ok, InsId} = z_db:insert(rsc, [{creator_id, z_acl:user(Context)} | InsProps], Context),
  351. InsId
  352. end,
  353. % Insert a category record for categories. Categories are so low level that we want
  354. % to make sure that all categories always have a category record attached.
  355. IsA = m_category:is_a(CategoryId, Context),
  356. IsCatInsert = case lists:member(category, IsA) of
  357. true ->
  358. m_hierarchy:append('$category', [InsertId], Context),
  359. true;
  360. false ->
  361. false
  362. end,
  363. % Place the inserted properties over the update properties, replacing duplicates.
  364. Props1 = lists:foldl(
  365. fun
  366. ({version, _}, Acc) -> Acc;
  367. ({creator_id, _}, Acc) -> Acc;
  368. ({P,_} = V, Acc) -> [ V | proplists:delete(P, Acc) ]
  369. end,
  370. Props,
  371. InsProps),
  372. update_transaction_fun_db(RscUpd, InsertId, Props1, InsProps, [], IsCatInsert, Context);
  373. update_transaction_fun_insert(#rscupd{id=Id} = RscUpd, Props, Raw, UpdateProps, Context) ->
  374. Props1 = proplists:delete(creator_id, Props),
  375. Props2 = case z_acl:is_admin(Context) of
  376. true ->
  377. case proplists:get_value(creator_id, UpdateProps) of
  378. self ->
  379. [{creator_id, Id} | Props1];
  380. CreatorId when is_integer(CreatorId) ->
  381. [{creator_id, CreatorId} | Props1 ];
  382. undefined ->
  383. Props1
  384. end;
  385. false ->
  386. Props1
  387. end,
  388. IsA = m_rsc:is_a(Id, Context),
  389. update_transaction_fun_expected(RscUpd, Id, Props2, Raw, IsA, false, Context).
  390. update_transaction_fun_expected(#rscupd{expected=Expected} = RscUpd, Id, Props1, Raw, IsA, false, Context) ->
  391. case check_expected(Raw, Expected, Context) of
  392. ok ->
  393. update_transaction_fun_db(RscUpd, Id, Props1, Raw, IsA, false, Context);
  394. {error, _} = Error ->
  395. Error
  396. end.
  397. check_expected(_Raw, [], _Context) ->
  398. ok;
  399. check_expected(Raw, [{Key,F}|Es], Context) when is_function(F) ->
  400. case F(Key, Raw, Context) of
  401. true -> check_expected(Raw, Es, Context);
  402. false -> {error, {expected, Key, proplists:get_value(Key, Raw)}}
  403. end;
  404. check_expected(Raw, [{Key,Value}|Es], Context) ->
  405. case proplists:get_value(Key, Raw) of
  406. Value -> check_expected(Raw, Es, Context);
  407. Other -> {error, {expected, Key, Other}}
  408. end.
  409. update_transaction_fun_db(RscUpd, Id, Props, Raw, IsABefore, IsCatInsert, Context) ->
  410. {version, Version} = proplists:lookup(version, Raw),
  411. UpdateProps = [ {version, Version+1} | proplists:delete(version, Props) ],
  412. UpdateProps1 = set_if_normal_update(RscUpd, modified, erlang:universaltime(), UpdateProps),
  413. UpdateProps2 = set_if_normal_update(RscUpd, modifier_id, z_acl:user(Context), UpdateProps1),
  414. {IsChanged, UpdatePropsN} = z_notifier:foldr(#rsc_update{
  415. action=case RscUpd#rscupd.id of
  416. insert_rsc -> insert;
  417. _ -> update
  418. end,
  419. id=Id,
  420. props=Raw
  421. },
  422. {false, UpdateProps2},
  423. Context),
  424. % Pre-pivot of the category-id to the category sequence nr.
  425. UpdatePropsN1 = case proplists:get_value(category_id, UpdatePropsN) of
  426. undefined ->
  427. UpdatePropsN;
  428. CatId ->
  429. CatNr = z_db:q1("select nr
  430. from hierarchy
  431. where id = $1
  432. and name = '$category'",
  433. [CatId],
  434. Context),
  435. [ {pivot_category_nr, CatNr} | UpdatePropsN]
  436. end,
  437. case RscUpd#rscupd.id =:= insert_rsc orelse IsChanged orelse is_changed(Raw, UpdatePropsN1) of
  438. true ->
  439. UpdatePropsPrePivoted = z_pivot_rsc:pivot_resource_update(Id, UpdatePropsN1, Raw, Context),
  440. {ok, 1} = z_db:update(rsc, Id, UpdatePropsPrePivoted, Context),
  441. ok = update_page_path_log(Id, Raw, UpdatePropsN, Context),
  442. {ok, Id, Raw, UpdatePropsN, IsABefore, IsCatInsert};
  443. false ->
  444. {ok, Id, notchanged}
  445. end.
  446. %% @doc Recombine all properties from the ones that are posted by a form.
  447. normalize_props(Id, Props, Context) ->
  448. normalize_props(Id, Props, [], Context).
  449. normalize_props(Id, Props, Options, Context) ->
  450. DateProps = recombine_dates(Id, Props, Context),
  451. TextProps = recombine_languages(DateProps, Context),
  452. BlockProps = recombine_blocks(TextProps, Props, Context),
  453. IsImport = proplists:get_value(is_import, Options, false),
  454. [ {map_property_name(IsImport, P), V} || {P, V} <- BlockProps ].
  455. set_if_normal_update(#rscupd{} = RscUpd, K, V, Props) ->
  456. set_if_normal_update_1(
  457. is_normal_update(RscUpd),
  458. K, V, Props).
  459. set_if_normal_update_1(false, _K, _V, Props) ->
  460. Props;
  461. set_if_normal_update_1(true, K, V, Props) ->
  462. [ {K,V} | proplists:delete(K, Props) ].
  463. is_normal_update(#rscupd{is_import=true}) -> false;
  464. is_normal_update(#rscupd{is_no_touch=true}) -> false;
  465. is_normal_update(#rscupd{}) -> true.
  466. %% @doc Check if the update will change the data in the database
  467. %% @spec is_changed(Current, Props) -> bool()
  468. is_changed(Current, Props) ->
  469. is_prop_changed(Props, Current).
  470. is_prop_changed([], _Current) ->
  471. false;
  472. is_prop_changed([{version, _}|Rest], Current) ->
  473. is_prop_changed(Rest, Current);
  474. is_prop_changed([{modifier_id, _}|Rest], Current) ->
  475. is_prop_changed(Rest, Current);
  476. is_prop_changed([{modified, _}|Rest], Current) ->
  477. is_prop_changed(Rest, Current);
  478. is_prop_changed([{pivot_category_nr, _}|Rest], Current) ->
  479. is_prop_changed(Rest, Current);
  480. is_prop_changed([{Prop, Value}|Rest], Current) ->
  481. case is_equal(Value, proplists:get_value(Prop, Current)) of
  482. true -> is_prop_changed(Rest, Current);
  483. false -> true % The property Prop has been changed.
  484. end.
  485. is_equal(A, A) -> true;
  486. is_equal(_, undefined) -> false;
  487. is_equal(undefined, _) -> false;
  488. is_equal(A,B) -> z_utils:are_equal(A, B).
  489. %% @doc Check if all props are acceptable. Examples are unique name, uri etc.
  490. %% @spec preflight_check(Id, Props, Context) -> ok | {error, Reason}
  491. preflight_check(insert_rsc, Props, Context) ->
  492. preflight_check(-1, Props, Context);
  493. preflight_check(_Id, [], _Context) ->
  494. ok;
  495. preflight_check(Id, [{name, Name}|T], Context) when Name =/= undefined ->
  496. case z_db:q1("select count(*) from rsc where name = $1 and id <> $2", [Name, Id], Context) of
  497. 0 ->
  498. preflight_check(Id, T, Context);
  499. _N ->
  500. lager:warning("[~p] Trying to insert duplicate name ~p",
  501. [z_context:site(Context), Name]),
  502. throw({error, duplicate_name})
  503. end;
  504. preflight_check(Id, [{page_path, Path}|T], Context) when Path =/= undefined ->
  505. case z_db:q1("select count(*) from rsc where page_path = $1 and id <> $2", [Path, Id], Context) of
  506. 0 ->
  507. preflight_check(Id, T, Context);
  508. _N ->
  509. lager:warning("[~p] Trying to insert duplicate page_path ~p",
  510. [z_context:site(Context), Path]),
  511. throw({error, duplicate_page_path})
  512. end;
  513. preflight_check(Id, [{uri, Uri}|T], Context) when Uri =/= undefined ->
  514. case z_db:q1("select count(*) from rsc where uri = $1 and id <> $2", [Uri, Id], Context) of
  515. 0 ->
  516. preflight_check(Id, T, Context);
  517. _N ->
  518. lager:warning("[~p] Trying to insert duplicate uri ~p",
  519. [z_context:site(Context), Uri]),
  520. throw({error, duplicate_uri})
  521. end;
  522. preflight_check(Id, [{'query', Query}|T], Context) ->
  523. Valid = case m_rsc:is_a(Id, 'query', Context) of
  524. true ->
  525. try
  526. search_query:search(search_query:parse_query_text(z_html:unescape(Query)), Context),
  527. true
  528. catch
  529. _: {error, {_, _}} ->
  530. false
  531. end;
  532. false -> true
  533. end,
  534. case Valid of
  535. true -> preflight_check(Id, T, Context);
  536. false -> throw({error, invalid_query})
  537. end;
  538. preflight_check(Id, [_H|T], Context) ->
  539. preflight_check(Id, T, Context).
  540. throw_if_category_not_allowed(_Id, _SafeProps, false, _Context) ->
  541. ok;
  542. throw_if_category_not_allowed(insert_rsc, SafeProps, _True, Context) ->
  543. case proplists:get_value(category_id, SafeProps) of
  544. undefined ->
  545. throw({error, nocategory});
  546. CatId ->
  547. throw_if_category_not_allowed_1(undefined, CatId, Context)
  548. end;
  549. throw_if_category_not_allowed(Id, SafeProps, _True, Context) ->
  550. case proplists:get_value(category_id, SafeProps) of
  551. undefined ->
  552. ok;
  553. CatId ->
  554. PrevCatId = z_db:q1("select category_id from rsc where id = $1", [Id], Context),
  555. throw_if_category_not_allowed_1(PrevCatId, CatId, Context)
  556. end.
  557. throw_if_category_not_allowed_1(CatId, CatId, _Context) ->
  558. ok;
  559. throw_if_category_not_allowed_1(_PrevCatId, CatId, Context) ->
  560. CategoryName = m_category:id_to_name(CatId, Context),
  561. case z_acl:is_allowed(insert, #acl_rsc{category=CategoryName}, Context) of
  562. true -> ok;
  563. _False -> throw({error, eaccess})
  564. end.
  565. %% @doc Remove whitespace around some predefined fields
  566. props_trim(Props) ->
  567. [
  568. case is_trimmable(P,V) of
  569. true -> {P, z_string:trim(V)};
  570. false -> {P,V}
  571. end
  572. || {P,V} <- Props
  573. ].
  574. %% @doc Remove properties the user is not allowed to change and convert some other to the correct data type
  575. %% @spec props_filter(Props1, Acc, Context) -> Props2
  576. props_filter([], Acc, _Context) ->
  577. Acc;
  578. props_filter([{uri, Uri}|T], Acc, Context) ->
  579. case Uri of
  580. Empty when Empty == undefined; Empty == []; Empty == <<>> ->
  581. props_filter(T, [{uri, undefined} | Acc], Context);
  582. _ ->
  583. props_filter(T, [{uri, z_sanitize:uri(Uri)} | Acc], Context)
  584. end;
  585. props_filter([{name, Name}|T], Acc, Context) ->
  586. case z_acl:is_allowed(use, mod_admin, Context) of
  587. true ->
  588. case Name of
  589. Empty when Empty == undefined; Empty == []; Empty == <<>> ->
  590. props_filter(T, [{name, undefined} | Acc], Context);
  591. _ ->
  592. props_filter(T, [{name, z_string:to_name(Name)} | Acc], Context)
  593. end;
  594. false ->
  595. props_filter(T, Acc, Context)
  596. end;
  597. props_filter([{page_path, Path}|T], Acc, Context) ->
  598. case z_acl:is_allowed(use, mod_admin, Context) of
  599. true ->
  600. case Path of
  601. Empty when Empty == undefined; Empty == []; Empty == <<>> ->
  602. props_filter(T, [{page_path, undefined} | Acc], Context);
  603. _ ->
  604. P = [ $/ | string:strip(z_utils:url_path_encode(Path), both, $/) ],
  605. props_filter(T, [{page_path, P} | Acc], Context)
  606. end;
  607. false ->
  608. props_filter(T, Acc, Context)
  609. end;
  610. props_filter([{slug, undefined}|T], Acc, Context) ->
  611. props_filter(T, [{slug, []} | Acc], Context);
  612. props_filter([{slug, <<>>}|T], Acc, Context) ->
  613. props_filter(T, [{slug, []} | Acc], Context);
  614. props_filter([{slug, ""}|T], Acc, Context) ->
  615. props_filter(T, [{slug, []} | Acc], Context);
  616. props_filter([{slug, Slug}|T], Acc, Context) ->
  617. props_filter(T, [{slug, to_slug(Slug, Context)} | Acc], Context);
  618. props_filter([{custom_slug, P}|T], Acc, Context) ->
  619. props_filter(T, [{custom_slug, z_convert:to_bool(P)} | Acc], Context);
  620. props_filter([{B, P}|T], Acc, Context)
  621. when B =:= is_published; B =:= is_featured; B=:= is_protected;
  622. B =:= is_dependent; B =:= is_query_live; B =:= date_is_all_day;
  623. B =:= is_website_redirect; B =:= is_page_path_multiple;
  624. B =:= is_authoritative ->
  625. props_filter(T, [{B, z_convert:to_bool(P)} | Acc], Context);
  626. props_filter([{P, DT}|T], Acc, Context)
  627. when P =:= created; P =:= modified;
  628. P =:= date_start; P =:= date_end;
  629. P =:= publication_start; P =:= publication_end ->
  630. props_filter(T, [{P,z_datetime:to_datetime(DT)}|Acc], Context);
  631. props_filter([{P, Id}|T], Acc, Context)
  632. when P =:= creator_id; P =:= modifier_id ->
  633. case m_rsc:rid(Id, Context) of
  634. undefined ->
  635. props_filter(T, Acc, Context);
  636. RId ->
  637. props_filter(T, [{P,RId}|Acc], Context)
  638. end;
  639. props_filter([{visible_for, Vis}|T], Acc, Context) ->
  640. VisibleFor = z_convert:to_integer(Vis),
  641. case VisibleFor of
  642. N when N >= 0 ->
  643. props_filter(T, [{visible_for, N} | Acc], Context);
  644. _ ->
  645. props_filter(T, Acc, Context)
  646. end;
  647. props_filter([{category, CatName}|T], Acc, Context) ->
  648. props_filter([{category_id, m_category:name_to_id_check(CatName, Context)} | T], Acc, Context);
  649. props_filter([{category_id, CatId}|T], Acc, Context) ->
  650. CatId1 = m_rsc:rid(CatId, Context),
  651. case m_rsc:is_a(CatId1, category, Context) of
  652. true ->
  653. props_filter(T, [{category_id, CatId1}|Acc], Context);
  654. false ->
  655. lager:error("[~p] Ignoring unknown category '~p' in update, using 'other' instead.",
  656. [z_context:site(Context), CatId]),
  657. props_filter(T, [{category_id,m_rsc:rid(other, Context)}|Acc], Context)
  658. end;
  659. props_filter([{content_group, undefined}|T], Acc, Context) ->
  660. props_filter(T, [{content_group_id, undefined}|Acc], Context);
  661. props_filter([{content_group, CgName}|T], Acc, Context) ->
  662. case m_rsc:rid(CgName, Context) of
  663. undefined ->
  664. lager:error("[~p] Ignoring unknown content group '~p' in update.",
  665. [z_context:site(Context), CgName]),
  666. props_filter(T, Acc, Context);
  667. CgId ->
  668. props_filter([{content_group_id, CgId}|T], Acc, Context)
  669. end;
  670. props_filter([{content_group_id, undefined}|T], Acc, Context) ->
  671. props_filter(T, [{content_group_id, undefined}|Acc], Context);
  672. props_filter([{content_group_id, CgId}|T], Acc, Context) ->
  673. CgId1 = m_rsc:rid(CgId, Context),
  674. case m_rsc:is_a(CgId1, content_group, Context)
  675. orelse m_rsc:is_a(CgId1, acl_collaboration_group, Context)
  676. of
  677. true ->
  678. props_filter(T, [{content_group_id, CgId1}|Acc], Context);
  679. false ->
  680. lager:error("[~p] Ignoring unknown content group '~p' in update.",
  681. [z_context:site(Context), CgId]),
  682. props_filter(T, Acc, Context)
  683. end;
  684. props_filter([{Location, P}|T], Acc, Context)
  685. when Location =:= location_lat; Location =:= location_lng ->
  686. X = try
  687. z_convert:to_float(P)
  688. catch
  689. _:_ -> undefined
  690. end,
  691. props_filter(T, [{Location, X} | Acc], Context);
  692. props_filter([{pref_language, Lang}|T], Acc, Context) ->
  693. Lang1 = case z_trans:to_language_atom(Lang) of
  694. {ok, LangAtom} -> LangAtom;
  695. {error, not_a_language} -> undefined
  696. end,
  697. props_filter(T, [{pref_language, Lang1} | Acc], Context);
  698. props_filter([{language, Langs}|T], Acc, Context) ->
  699. props_filter(T, [{language, filter_languages(Langs)}|Acc], Context);
  700. props_filter([{_Prop, _V}=H|T], Acc, Context) ->
  701. props_filter(T, [H|Acc], Context).
  702. %% Filter all given languages, drop unknown languages.
  703. %% Ensure that the languages are a list of atoms.
  704. filter_languages([]) -> [];
  705. filter_languages(<<>>) -> [];
  706. filter_languages(Lang) when is_binary(Lang); is_atom(Lang) ->
  707. filter_languages([Lang]);
  708. filter_languages([C|_] = Lang) when is_integer(C) ->
  709. filter_languages([Lang]);
  710. filter_languages([L|_] = Langs) when is_list(L); is_binary(L); is_atom(L) ->
  711. lists:foldr(
  712. fun(Lang, Acc) ->
  713. case z_trans:to_language_atom(Lang) of
  714. {ok, LangAtom} -> [LangAtom|Acc];
  715. {error, not_a_language} -> Acc
  716. end
  717. end,
  718. [],
  719. Langs).
  720. %% @doc Automatically modify some props on update.
  721. %% @spec props_autogenerate(Id, Props1, Context) -> Props2
  722. props_autogenerate(Id, Props, Context) ->
  723. %% When title is updating, check the rsc 'custom_slug' field to see if we need to update the slug or not.
  724. Props1 = case proplists:get_value(title, Props) of
  725. undefined -> Props;
  726. Title ->
  727. case {proplists:get_value(custom_slug, Props), m_rsc:p(custom_slug, Id, Context)} of
  728. {true, _} -> Props;
  729. {_, true} -> Props;
  730. _X ->
  731. %% Determine the slug from the title.
  732. [{slug, to_slug(Title, Context)} | proplists:delete(slug, Props)]
  733. end
  734. end,
  735. Props1.
  736. %% @doc Fill in some defaults for empty props on insert.
  737. %% @spec props_defaults(Props1, Context) -> Props2
  738. props_defaults(Props, Context) ->
  739. % Generate slug from the title (when there is a title)
  740. Props1 = case proplists:get_value(slug, Props) of
  741. undefined ->
  742. case proplists:get_value(title, Props) of
  743. undefined ->
  744. Props;
  745. Title ->
  746. lists:keystore(slug, 1, Props, {slug, to_slug(Title, Context)})
  747. end;
  748. _ ->
  749. Props
  750. end,
  751. % Assume content is authoritative, unless stated otherwise
  752. case proplists:get_value(is_authoritative, Props1) of
  753. undefined -> [{is_authoritative, true}|Props1];
  754. _ -> Props
  755. end.
  756. props_filter_protected(Props, RscUpd) ->
  757. IsNormal = is_normal_update(RscUpd),
  758. lists:filter(fun
  759. ({K, _}) -> not is_protected(K, IsNormal)
  760. end,
  761. Props).
  762. to_slug(undefined, _Context) -> undefined;
  763. to_slug({trans, _} = Tr, Context) -> to_slug(z_trans:lookup_fallback(Tr, en, Context), Context);
  764. to_slug(B, _Context) when is_binary(B) -> truncate_slug(z_string:to_slug(B));
  765. to_slug(X, Context) -> to_slug(z_convert:to_binary(X), Context).
  766. truncate_slug(<<Slug:78/binary, _/binary>>) -> Slug;
  767. truncate_slug(Slug) -> Slug.
  768. %% @doc Map property names to an atom, fold pivot and computed fields together for later filtering.
  769. map_property_name(IsImport, P) when not is_list(P) -> map_property_name(IsImport, z_convert:to_list(P));
  770. map_property_name(_IsImport, "computed_"++_) -> computed_xxx;
  771. map_property_name(_IsImport, "pivot_"++_) -> pivot_xxx;
  772. map_property_name(false, P) when is_list(P) -> erlang:list_to_existing_atom(P);
  773. map_property_name(true, P) when is_list(P) -> erlang:list_to_atom(P).
  774. %% @doc Properties that can't be updated with m_rsc_update:update/3 or m_rsc_update:insert/2
  775. is_protected(id, _IsNormal) -> true;
  776. is_protected(created, true) -> true;
  777. is_protected(creator_id, true) -> true;
  778. is_protected(modified, true) -> true;
  779. is_protected(modifier_id, true) -> true;
  780. is_protected(props, _IsNormal) -> true;
  781. is_protected(version, _IsNormal) -> true;
  782. is_protected(page_url, _IsNormal) -> true;
  783. is_protected(medium, _IsNormal) -> true;
  784. is_protected(pivot_xxx, _IsNormal) -> true;
  785. is_protected(computed_xxx, _IsNormal) -> true;
  786. is_protected(_, _IsNormal) -> false.
  787. is_trimmable(_, V) when not is_binary(V), not is_list(V) -> false;
  788. is_trimmable(title, _) -> true;
  789. is_trimmable(title_short, _) -> true;
  790. is_trimmable(summary, _) -> true;
  791. is_trimmable(chapeau, _) -> true;
  792. is_trimmable(subtitle, _) -> true;
  793. is_trimmable(email, _) -> true;
  794. is_trimmable(uri, _) -> true;
  795. is_trimmable(website, _) -> true;
  796. is_trimmable(page_path, _) -> true;
  797. is_trimmable(name, _) -> true;
  798. is_trimmable(slug, _) -> true;
  799. is_trimmable(custom_slug, _) -> true;
  800. is_trimmable(category, _) -> true;
  801. is_trimmable(rsc_id, _) -> true;
  802. is_trimmable(_, _) -> false.
  803. %% @doc Combine all textual date fields into real date. Convert them to UTC afterwards.
  804. recombine_dates(Id, Props, Context) ->
  805. LocalNow = z_datetime:to_local(erlang:universaltime(), Context),
  806. {Dates, Props1} = recombine_dates_1(Props, [], []),
  807. {Dates1, DateGroups} = group_dates(Dates),
  808. {DateGroups1, DatesNull} = collect_empty_date_groups(DateGroups, [], []),
  809. {Dates2, DatesNull1} = collect_empty_dates(Dates1, [], DatesNull),
  810. Dates3 = [ {Name, date_from_default(LocalNow, D)} || {Name, D} <- Dates2 ],
  811. DateGroups2 = [ {Name, dategroup_fill_parts(date_from_default(LocalNow, S), E)} || {Name, {S,E}} <- DateGroups1 ],
  812. Dates4 = lists:foldl(
  813. fun({Name, {S, E}}, Acc) ->
  814. [
  815. {Name++"_start", S},
  816. {Name++"_end", E}
  817. | Acc
  818. ]
  819. end,
  820. Dates3,
  821. DateGroups2),
  822. DatesUTC = maybe_dates_to_utc(Id, Dates4, Props, Context),
  823. [
  824. {tz, z_context:tz(Context)}
  825. | DatesUTC ++ DatesNull1 ++ Props1
  826. ].
  827. maybe_dates_to_utc(Id, Dates, Props, Context) ->
  828. IsAllDay = is_all_day(Id, Props, Context),
  829. [ maybe_to_utc(IsAllDay, NameDT,Context) || NameDT <- Dates ].
  830. maybe_to_utc(true, {"date_start", _Date} = D, _Context) ->
  831. D;
  832. maybe_to_utc(true, {"date_end", _Date} = D, _Context) ->
  833. D;
  834. maybe_to_utc(_IsAllDay, {Name, Date}, Context) ->
  835. {Name, z_datetime:to_utc(Date, Context)}.
  836. is_all_day(Id, Props, Context) ->
  837. case proplists:get_value(date_is_all_day, Props) of
  838. undefined ->
  839. case proplists:get_value("date_is_all_day", Props) of
  840. undefined ->
  841. case is_integer(Id) of
  842. false ->
  843. false;
  844. true ->
  845. z_convert:to_bool(m_rsc:p_no_acl(Id, date_is_all_day, Context))
  846. end;
  847. IsAllDay ->
  848. z_convert:to_bool(IsAllDay)
  849. end;
  850. IsAllDay ->
  851. z_convert:to_bool(IsAllDay)
  852. end.
  853. collect_empty_date_groups([], Acc, Null) ->
  854. {Acc, Null};
  855. collect_empty_date_groups([{"publication", _} = R|T], Acc, Null) ->
  856. collect_empty_date_groups(T, [R|Acc], Null);
  857. collect_empty_date_groups([{Name, {
  858. {{undefined, undefined, undefined}, {undefined, undefined, undefined}},
  859. {{undefined, undefined, undefined}, {undefined, undefined, undefined}}
  860. }}|T], Acc, Null) ->
  861. collect_empty_date_groups(T, Acc, [{Name++"_start", undefined}, {Name++"_end", undefined} | Null]);
  862. collect_empty_date_groups([H|T], Acc, Null) ->
  863. collect_empty_date_groups(T, [H|Acc], Null).
  864. collect_empty_dates([], Acc, Null) ->
  865. {Acc, Null};
  866. collect_empty_dates([{Name, {{undefined, undefined, undefined}, {undefined, undefined, undefined}}}|T], Acc, Null) ->
  867. collect_empty_dates(T, Acc, [{Name, undefined}|Null]);
  868. collect_empty_dates([H|T], Acc, Null) ->
  869. collect_empty_dates(T, [H|Acc], Null).
  870. recombine_dates_1([], Dates, Acc) ->
  871. {Dates, Acc};
  872. recombine_dates_1([{"dt:"++K,V}|T], Dates, Acc) ->
  873. [Part, End, Name] = string:tokens(K, ":"),
  874. Dates1 = recombine_date(Part, End, Name, V, Dates),
  875. recombine_dates_1(T, Dates1, Acc);
  876. recombine_dates_1([H|T], Dates, Acc) ->
  877. recombine_dates_1(T, Dates, [H|Acc]).
  878. recombine_date(Part, End, Name, undefined, Dates) ->
  879. recombine_date(Part, End, Name, "", Dates);
  880. recombine_date(Part, _End, Name, V, Dates) ->
  881. Date = case proplists:get_value(Name, Dates) of
  882. undefined ->
  883. {{undefined, undefined, undefined}, {undefined, undefined, undefined}};
  884. D ->
  885. D
  886. end,
  887. Date1 = recombine_date_part(Date, Part, to_date_value(Part, string:strip(V))),
  888. lists:keystore(Name, 1, Dates, {Name, Date1}).
  889. recombine_date_part({{_Y,M,D},{H,I,S}}, "y", V) -> {{V,M,D},{H,I,S}};
  890. recombine_date_part({{Y,_M,D},{H,I,S}}, "m", V) -> {{Y,V,D},{H,I,S}};
  891. recombine_date_part({{Y,M,_D},{H,I,S}}, "d", V) -> {{Y,M,V},{H,I,S}};
  892. recombine_date_part({{Y,M,D},{_H,I,S}}, "h", V) -> {{Y,M,D},{V,I,S}};
  893. recombine_date_part({{Y,M,D},{H,_I,S}}, "i", V) -> {{Y,M,D},{H,V,S}};
  894. recombine_date_part({{Y,M,D},{H,I,_S}}, "s", V) -> {{Y,M,D},{H,I,V}};
  895. recombine_date_part({{Y,M,D},{_H,_I,S}}, "hi", {H,I,_S}) -> {{Y,M,D},{H,I,S}};
  896. recombine_date_part({{Y,M,D},_Time}, "his", {_,_,_} = V) -> {{Y,M,D},V};
  897. recombine_date_part({_Date,{H,I,S}}, "ymd", {_,_,_} = V) -> {V,{H,I,S}}.
  898. to_date_value(Part, V) when Part == "ymd" orelse Part == "his"->
  899. case string:tokens(V, "-/: ") of
  900. [] -> {undefined, undefined, undefined};
  901. [Y,M,D] -> {to_int(Y), to_int(M), to_int(D)}
  902. end;
  903. to_date_value("hi", V) ->
  904. case string:tokens(V, "-/: ") of
  905. [] -> {undefined, undefined, undefined};
  906. [H] -> {to_int(H), 0, undefined};
  907. [H,I] -> {to_int(H), to_int(I), undefined}
  908. end;
  909. to_date_value(_, V) ->
  910. to_int(V).
  911. group_dates(Dates) ->
  912. group_dates(Dates, [], []).
  913. group_dates([], Groups, Acc) ->
  914. {Acc, Groups};
  915. group_dates([{Name,D}|T], Groups, Acc) ->
  916. case lists:suffix("_start", Name) of
  917. true ->
  918. Base = lists:sublist(Name, length(Name) - 6),
  919. Range = case proplists:get_value(Base, Groups) of
  920. {_Start, End} ->
  921. { D, End };
  922. undefined ->
  923. { D, {{undefined, undefined, undefined}, {undefined, undefined, undefined}} }
  924. end,
  925. Groups1 = lists:keystore(Base, 1, Groups, {Base, Range}),
  926. group_dates(T, Groups1, Acc);
  927. false ->
  928. case lists:suffix("_end", Name) of
  929. true ->
  930. Base = lists:sublist(Name, length(Name) - 4),
  931. Range = case proplists:get_value(Base, Groups) of
  932. {Start, _End} ->
  933. { Start, D };
  934. undefined ->
  935. { {{undefined, undefined, undefined}, {0, 0, 0}}, D }
  936. end,
  937. Groups1 = lists:keystore(Base, 1, Groups, {Base, Range}),
  938. group_dates(T, Groups1, Acc);
  939. false ->
  940. group_dates(T, Groups, [{Name,D}|Acc])
  941. end
  942. end.
  943. dategroup_fill_parts( S, {{undefined,undefined,undefined},{undefined,undefined,undefined}} ) ->
  944. {S, ?ST_JUTTEMIS};
  945. dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{undefined,Me,De},{He,Ie,Se}} ) ->
  946. dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ys,Me,De},{He,Ie,Se}} );
  947. dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,undefined,De},{He,Ie,Se}} ) ->
  948. dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}} ,{{Ye,Ms,De},{He,Ie,Se}} );
  949. dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,undefined},{He,Ie,Se}} ) ->
  950. dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,Ds},{He,Ie,Se}} );
  951. dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{undefined,Ie,Se}} ) ->
  952. dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{23,Ie,Se}} );
  953. dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,undefined,Se}} ) ->
  954. dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,59,Se}} );
  955. dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,Ie,undefined}} ) ->
  956. dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,Ie,59}} );
  957. dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,Ie,Se}} ) ->
  958. {{{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,Ie,Se}}}.
  959. date_from_default( S, {{undefined,undefined,undefined},{undefined,undefined,undefined}} ) ->
  960. S;
  961. date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{undefined,Me,De},{He,Ie,Se}} ) ->
  962. date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ys,Me,De},{He,Ie,Se}} );
  963. date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,undefined,De},{He,Ie,Se}} ) ->
  964. date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Ms,De},{He,Ie,Se}} );
  965. date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,undefined},{He,Ie,Se}} ) ->
  966. date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,Ds},{He,Ie,Se}} );
  967. date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{undefined,Ie,Se}} ) ->
  968. date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{0,Ie,Se}} );
  969. date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,undefined,Se}} ) ->
  970. date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,0,Se}} );
  971. date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,Ie,undefined}} ) ->
  972. date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,Ie,0}} );
  973. date_from_default( _S, {{Ye,Me,De},{He,Ie,Se}} ) ->
  974. {{Ye,Me,De},{He,Ie,Se}}.
  975. to_int("") ->
  976. undefined;
  977. to_int(<<>>) ->
  978. undefined;
  979. to_int(A) ->
  980. try
  981. list_to_integer(A)
  982. catch
  983. _:_ -> undefined
  984. end.
  985. % to_datetime(undefined) ->
  986. % erlang:universaltime();
  987. % to_datetime(B) ->
  988. % case z_datetime:to_datetime(B) of
  989. % undefined -> erlang:universaltime();
  990. % DT -> DT
  991. % end.
  992. %% @doc get all languages encoded in proplists' keys.
  993. %% e.g. m_rsc_update:props_languages([{"foo$en", x}, {"bar$nl", x}]) -> ["en", "nl"]
  994. props_languages(Props) ->
  995. lists:foldr(fun({Key, _}, Acc) ->
  996. case string:tokens(z_convert:to_list(Key), [$$]) of
  997. [_, Lang] ->
  998. case lists:member(Lang, Acc) of
  999. true -> Acc;
  1000. false -> [Lang|Acc]
  1001. end;
  1002. _ -> Acc
  1003. end
  1004. end, [], Props).
  1005. %% @doc Combine language versions of texts. Assume we edit all texts or none.
  1006. recombine_languages(Props, Context) ->
  1007. case props_languages(Props) of
  1008. [] ->
  1009. Props;
  1010. L ->
  1011. Cfg = [ atom_to_list(Code) || Code <- config_langs(Context) ],
  1012. L1 = filter_langs(edited_languages(Props, L), Cfg),
  1013. {LangProps, OtherProps} = comb_lang(Props, L1, [], []),
  1014. LangProps ++ [{language, [list_to_atom(Lang) || Lang <- L1]}|proplists:delete("language", OtherProps)]
  1015. end.
  1016. %% @doc Fetch all the edited languages, from 'language' inputs or a merged 'language' property
  1017. edited_languages(Props, PropLangs) ->
  1018. case proplists:is_defined("language", Props) of
  1019. true ->
  1020. proplists:get_all_values("language", Props);
  1021. false ->
  1022. case proplists:get_value(language, Props) of
  1023. L when is_list(L) ->
  1024. [ z_convert:to_list(Lang) || Lang <- L ];
  1025. undefined ->
  1026. PropLangs
  1027. end
  1028. end.
  1029. comb_lang([], _L1, LAcc, OAcc) ->
  1030. {LAcc, OAcc};
  1031. comb_lang([{P,V}|Ps], L1, LAcc, OAcc) when is_list(P) ->
  1032. case string:tokens(P, "$") of
  1033. [P1,Lang] ->
  1034. case lists:member(Lang, L1) of
  1035. true -> comb_lang(Ps, L1, append_langprop(P1, Lang, V, LAcc), OAcc);
  1036. false -> comb_lang(Ps, L1, LAcc, OAcc)
  1037. end;
  1038. _ ->
  1039. comb_lang(Ps, L1, LAcc, [{P,V}|OAcc])
  1040. end;
  1041. comb_lang([PV|Ps], L1, LAcc, OAcc) ->
  1042. comb_lang(Ps, L1, LAcc, [PV|OAcc]).
  1043. append_langprop(P, Lang, V, Acc) ->
  1044. Lang1 = list_to_atom(Lang),
  1045. case proplists:get_value(P, Acc) of
  1046. {trans, Tr} ->
  1047. Tr1 = [{Lang1, z_convert:to_binary(V)}|Tr],
  1048. [{P, {trans, Tr1}} | proplists:delete(P, Acc)];
  1049. undefined ->
  1050. [{P, {trans, [{Lang1,z_convert:to_binary(V)}]}}|Acc]
  1051. end.
  1052. recombine_blocks(Props, OrgProps, Context) ->
  1053. Props1 = recombine_blocks_form(Props, OrgProps, Context),
  1054. recombine_blocks_import(Props1, OrgProps, Context).
  1055. recombine_blocks_form(Props, OrgProps, Context) ->
  1056. {BPs, Ps} = lists:partition(fun({"block-"++ _, _}) -> true; (_) -> false end, Props),
  1057. case BPs of
  1058. [] ->
  1059. case proplists:get_value(blocks, Props) of
  1060. Blocks when is_list(Blocks) ->
  1061. Blocks1 = [ {proplists:get_value(name, B),B} || B <- Blocks ],
  1062. z_utils:prop_replace(blocks, normalize_blocks(Blocks1, Context), Props);
  1063. _ ->
  1064. Props
  1065. end;
  1066. _ ->
  1067. Keys = block_ids(OrgProps, []),
  1068. Dict = lists:foldr(
  1069. fun ({"block-", _}, Acc) ->
  1070. Acc;
  1071. ({"block-"++Name, Val}, Acc) ->
  1072. Ts = string:tokens(Name, "-"),
  1073. BlockId = iolist_to_binary(tl(lists:reverse(Ts))),
  1074. BlockField = lists:last(Ts),
  1075. dict:append(BlockId, {BlockField, Val}, Acc)
  1076. end,
  1077. dict:new(),
  1078. BPs),
  1079. Blocks = normalize_blocks([ {K, dict:fetch(K, Dict)} || K <- Keys ], Context),
  1080. [{blocks, Blocks++proplists:get_value(blocks, Ps, [])} | proplists:delete(blocks, Ps) ]
  1081. end.
  1082. recombine_blocks_import(Props, _OrgProps, Context) ->
  1083. {BPs, Ps} = lists:partition(fun({"blocks."++ _, _}) -> true; (_) -> false end, Props),
  1084. case BPs of
  1085. [] ->
  1086. Props;
  1087. _ ->
  1088. {Dict,Keys} = lists:foldr(
  1089. fun({"blocks."++Name, Val}, {Acc,KeyAcc}) ->
  1090. [BlockId,BlockField] = string:tokens(Name, "."),
  1091. KeyAcc1 = case lists:member(BlockId, KeyAcc) of
  1092. true -> KeyAcc;
  1093. false -> [ BlockId | KeyAcc ]
  1094. end,
  1095. {dict:append(BlockId, {BlockField, Val}, Acc), KeyAcc1}
  1096. end,
  1097. {dict:new(),[]},
  1098. BPs),
  1099. Blocks = normalize_blocks([ {K, dict:fetch(K, Dict)} || K <- Keys ], Context),
  1100. [{blocks, Blocks++proplists:get_value(blocks, Ps, [])} | proplists:delete(blocks, Ps) ]
  1101. end.
  1102. block_ids([], Acc) ->
  1103. lists:reverse(Acc);
  1104. block_ids([{"block-"++Name,_}|Rest], Acc) when Name =/= [] ->
  1105. Ts = string:tokens(Name, "-"),
  1106. BlockId = iolist_to_binary(tl(lists:reverse(Ts))),
  1107. case lists:member(BlockId, Acc) of
  1108. true -> block_ids(Rest, Acc);
  1109. false -> block_ids(Rest, [BlockId|Acc])
  1110. end;
  1111. block_ids([_|Rest], Acc) ->
  1112. block_ids(Rest, Acc).
  1113. normalize_blocks(Blocks, Context) ->
  1114. Blocks1 = lists:map(
  1115. fun({Name,B}) ->
  1116. normalize_block(Name, B, Context)
  1117. end,
  1118. Blocks),
  1119. lists:filter(fun(B) ->
  1120. case proplists:get_value(type, B) of
  1121. <<>> -> false;
  1122. undefined -> false;
  1123. _ -> true
  1124. end
  1125. end,
  1126. Blocks1).
  1127. normalize_block(Name, B, Context) ->
  1128. Props = lists:map(fun
  1129. ({rsc_id, V}) -> {rsc_id, m_rsc:rid(V, Context)};
  1130. ({"rsc_id", V}) -> {rsc_id, m_rsc:rid(V, Context)};
  1131. ({<<"rsc_id">>, V}) -> {rsc_id, m_rsc:rid(V, Context)};
  1132. ({"is_" ++ _ = K, V}) -> {to_existing_atom(K), z_convert:to_bool(V)};
  1133. ({<<"is_", _/binary>> = K, V}) -> {to_existing_atom(K), z_convert:to_bool(V)};
  1134. ({K, V}) when is_list(K); is_binary(K) -> {to_existing_atom(K), V};
  1135. (Pair) -> Pair
  1136. end,
  1137. B),
  1138. case proplists:is_defined(name, Props) of
  1139. true -> Props;
  1140. false -> [{name, Name} | Props]
  1141. end.
  1142. to_existing_atom(K) when is_binary(K) ->
  1143. binary_to_existing_atom(K, utf8);
  1144. to_existing_atom(K) when is_list(K) ->
  1145. list_to_existing_atom(K);
  1146. to_existing_atom(K) when is_atom(K) ->
  1147. K.
  1148. %% @doc Accept only configured languages
  1149. filter_langs(L, Cfg) ->
  1150. lists:filter(fun(LangS) ->
  1151. lists:member(LangS, Cfg)
  1152. end,
  1153. L).
  1154. config_langs(Context) ->
  1155. case m_config:get(i18n, language_list, Context) of
  1156. undefined -> [en];
  1157. Cfg -> [ Code || {Code, _} <- proplists:get_value(list, Cfg, [{en,[]}]) ]
  1158. end.
  1159. update_page_path_log(RscId, OldProps, NewProps, Context) ->
  1160. Old = proplists:get_value(page_path, OldProps),
  1161. New = proplists:get_value(page_path, NewProps, not_updated),
  1162. case {Old, New} of
  1163. {_, not_updated} ->
  1164. ok;
  1165. {Old, Old} ->
  1166. %% not changed
  1167. ok;
  1168. {undefined, _} ->
  1169. %% no old page path
  1170. ok;
  1171. {Old, New} ->
  1172. %% update
  1173. z_db:q("DELETE FROM rsc_page_path_log WHERE page_path = $1 OR page_path = $2", [New, Old], Context),
  1174. z_db:q("INSERT INTO rsc_page_path_log(id, page_path) VALUES ($1, $2)", [RscId, Old], Context),
  1175. ok
  1176. end.
  1177. test() ->
  1178. [{"publication_start",{{2009,7,9},{0,0,0}}},
  1179. {"publication_end",?ST_JUTTEMIS},
  1180. {"plop","hello"}]
  1181. = recombine_dates(insert_rsc, [
  1182. {"dt:y:0:publication_start", "2009"},
  1183. {"dt:m:0:publication_start", "7"},
  1184. {"dt:d:0:publication_start", "9"},
  1185. {"dt:y:1:publication_end", ""},
  1186. {"dt:m:1:publication_end", ""},
  1187. {"dt:d:1:publication_end", ""},
  1188. {"plop", "hello"}
  1189. ], z_context:new_tests()),
  1190. ok.