--
-- MySQL stored functions and triggers for DBMS-side checking of IntraACL per-page permissions
-- Author: Vitaliy Filippov, 2013+
-- License: GNU LGPLv3 or newer
--

DELIMITER //

-- old permission checker procedure (not needed anymore)
drop function if exists check_read_right //

--
-- Stored procedure for creating parent_pages table
--
drop procedure if exists /*_*/create_parent_pages //

create procedure /*_*/create_parent_pages() modifies sql data
begin
  create table /*_*/parent_pages (
    parent_page_id int unsigned not null,
    page_id int unsigned not null,
    primary key (page_id),
    foreign key (page_id) references /*_*/page (page_id) on delete cascade on update cascade,
    foreign key (parent_page_id) references /*_*/page (page_id) on delete cascade on update cascade
  );
  insert into /*_*/parent_pages (parent_page_id, page_id)
    select p1.page_id, p2.page_id
    from /*_*/page p1
    inner join /*_*/intraacl_rules r1 on r1.pe_type=10 and r1.pe_id=p1.page_id and r1.child_type=6 and r1.child_id=0
    inner join /*_*/page p2 on p2.page_namespace=p1.page_namespace
      and p2.page_title like concat(replace(replace(p1.page_title, '%', '\%'), '_', '\_'), '/%')
    left join /*_*/intraacl_rules r2 on r2.pe_type=10 and r2.pe_id=p2.page_id and r2.child_type=6 and r2.child_id=0
    left join /*_*/page p3 on p3.page_namespace=p1.page_namespace
      and p3.page_title like concat(replace(replace(p1.page_title, '%', '\%'), '_', '\_'), '/%')
      and p2.page_title like concat(replace(replace(p3.page_title, '%', '\%'), '_', '\_'), '/%')
    left join /*_*/intraacl_rules r3 on r3.pe_type=10 and r3.pe_id=p3.page_id and r3.child_type=6 and r3.child_id=0
    where r2.pe_id is null and (p3.page_id is null or r3.pe_id is null)
    union all
    select p1.page_id, p1.page_id
      from /*_*/page p1
      inner join /*_*/intraacl_rules r1 on r1.pe_type=10 and r1.pe_id=p1.page_id and r1.child_type=6 and r1.child_id=0;
end //

drop table if exists /*_*/parent_pages //

call /*_*/create_parent_pages() //

drop procedure if exists /*_*/refresh_parent_pages_for_children //

create procedure /*_*/refresh_parent_pages_for_children(_page_id int unsigned, _page_namespace int, _page_title text)
modifies sql data
begin
  replace into /*_*/parent_pages (parent_page_id, page_id)
    select _page_id, p2.page_id
      from /*_*/page p2
      inner join /*_*/intraacl_rules r1 on r1.pe_type=10 and r1.pe_id=_page_id and r1.child_type=6 and r1.child_id=0
      left join /*_*/intraacl_rules r2 on r2.pe_type=10 and r2.pe_id=p2.page_id and r2.child_type=6 and r2.child_id=0
      left join /*_*/page p3 on p3.page_namespace=_page_namespace
        and p3.page_title like concat(replace(replace(_page_title, '%', '\%'), '_', '\_'), '/%')
        and p2.page_title like concat(replace(replace(p3.page_title, '%', '\%'), '_', '\_'), '/%')
      left join /*_*/intraacl_rules r3 on r3.pe_type=10 and r3.pe_id=p3.page_id and r3.child_type=6 and r3.child_id=0
      where r2.pe_id is null and p2.page_namespace=_page_namespace
        and p2.page_title like concat(replace(replace(_page_title, '%', '\%'), '_', '\_'), '/%')
        and (p3.page_id is null or r3.pe_id is null)
    union all
    select _page_id, p2.page_id
      from /*_*/page p2
      inner join /*_*/intraacl_rules r1 on r1.pe_type=10 and r1.pe_id=_page_id and r1.child_type=6 and r1.child_id=0
      where p2.page_id=_page_id;
end //

drop procedure if exists /*_*/refresh_parent_for_page //

create procedure /*_*/refresh_parent_for_page(_page_id int unsigned, _page_namespace int, _page_title text)
modifies sql data
begin
  declare _parent_id int unsigned default null;
  select /*_*/get_parent_for_page(_page_namespace, _page_title) into _parent_id;
  if _parent_id is not null then
    replace into /*_*/parent_pages (parent_page_id, page_id) values (_parent_id, _page_id);
  else
    delete from /*_*/parent_pages where page_id=_page_id;
  end if;
end //

drop function if exists /*_*/get_parent_for_page //

create function /*_*/get_parent_for_page(_page_namespace int, _page_title text) returns int unsigned
reads sql data
begin
  declare prev int default 0;
  declare pos int default 0;
  declare _parent_id int unsigned default null;
  repeat
    set prev = pos;
    select p1.page_id from /*_*/page p1
      inner join /*_*/intraacl_rules r1 on r1.pe_type=10 and r1.pe_id=p1.page_id and r1.child_type=6 and r1.child_id=0
      where p1.page_namespace=_page_namespace
      and p1.page_title = substr(_page_title, 1, length(_page_title)-pos)
      into _parent_id;
    if _parent_id is not null then
      set pos = 0;
    else
      set pos = locate('/', reverse(_page_title), prev+1);
    end if;
  until pos <= 0 end repeat;
  return _parent_id;
end //

drop procedure if exists /*_*/refresh_all_parents_for_page //

create procedure /*_*/refresh_all_parents_for_page(_page_id int unsigned, _page_namespace int, _page_title text)
modifies sql data
begin
  if _page_namespace is null or _page_title is null then
    select page_namespace, page_title from page where page_id=_page_id into _page_namespace, _page_title;
  end if;
  call /*_*/refresh_parent_pages_for_children(_page_id, _page_namespace, _page_title);
  call /*_*/refresh_parent_for_page(_page_id, _page_namespace, _page_title);
end //

drop procedure if exists /*_*/refresh_parent_pages_for_parent_children //

create procedure /*_*/refresh_parent_pages_for_parent_children(_page_id int unsigned, _page_namespace int, _page_title text)
modifies sql data
begin
  declare _parent_id int unsigned default null;
  if _page_namespace is null or _page_title is null then
    select page_namespace, page_title from page where page_id=_page_id into _page_namespace, _page_title;
  end if;
  select /*_*/get_parent_for_page(_page_namespace, _page_title) into _parent_id;
  if _parent_id is null then
    delete from /*_*/parent_pages where parent_page_id=_page_id;
  else
    update /*_*/parent_pages set parent_page_id=_parent_id where parent_page_id=_page_id;
  end if;
end //

--
-- Stored procedure for creating category_closure table
--
drop procedure if exists /*_*/create_category_closure //

create procedure /*_*/create_category_closure() modifies sql data
begin
  declare n bigint default 0;
  declare prev bigint default 0;
  create table /*_*/category_closure as select cl_from page_id, page_id category_id
    from /*_*/categorylinks, /*_*/page where page_namespace=14 and page_title=cl_to;
  alter table /*_*/category_closure add primary key (page_id, category_id);
  alter table /*_*/category_closure add key (category_id);
  repeat
    set prev = n;
    replace into /*_*/category_closure (page_id, category_id)
      select c1.page_id, c2.category_id
      from /*_*/category_closure c1, /*_*/category_closure c2
      where c1.category_id=c2.page_id;
    select count(*) from /*_*/category_closure into n;
  until n <= prev end repeat;
end //

drop table if exists /*_*/category_closure //

call /*_*/create_category_closure() //

--
-- Triggers for maintaining category_closure and parent_pages tables in actual state
--

drop procedure if exists /*_*/fill_category_closure //

-- add all categories for changed_page_id
create procedure /*_*/fill_category_closure(changed_page_id int unsigned)
begin
  declare n bigint default 0;
  declare prev bigint default 0;
  insert into /*_*/category_closure (page_id, category_id)
    select changed_page_id, page_id
    from /*_*/categorylinks c, /*_*/page
    where cl_from=changed_page_id and cl_to=page_title and page_namespace=14
    on duplicate key update page_id=values(page_id);
  select count(*) from /*_*/category_closure where page_id=changed_page_id into n;
  while n > prev do
    set prev = n;
    insert into /*_*/category_closure (page_id, category_id)
      select changed_page_id, c2.category_id
      from /*_*/category_closure c1, /*_*/category_closure c2
      where c1.page_id=changed_page_id and c1.category_id=c2.page_id
      on duplicate key update page_id=values(page_id);
    select count(*) from /*_*/category_closure where page_id=changed_page_id into n;
  end while;
end //
drop procedure if exists /*_*/do_insert_category_closure_catlinks //

create procedure /*_*/do_insert_category_closure_catlinks(pg_id int unsigned, cat_id int unsigned)
modifies sql data
begin
  -- add the (pg_id -> cat_id) edge
  insert into /*_*/category_closure values (pg_id, cat_id)
    on duplicate key update page_id=values(page_id);
  -- add indirect edges: (pg_id -> cat_id) (cat_id -> *2) --> (pg_id -> *2)
  insert into /*_*/category_closure
    select pg_id, c1.category_id
    from /*_*/category_closure c1
    where c1.page_id=cat_id
    on duplicate key update page_id=values(page_id);
  -- add indirect edges: (*1 -> pg_id) (pg_id -> cat_id) --> (*1 -> cat_id)
  insert into /*_*/category_closure
    select c1.page_id, cat_id
    from /*_*/category_closure c1
    where c1.category_id=pg_id
    on duplicate key update page_id=values(page_id);
  -- add indirect edges: (*1 -> pg_id) (cat_id -> *2) --> (*1 -> *2)
  insert into /*_*/category_closure
    select c1.page_id, c2.category_id
    from /*_*/category_closure c1, /*_*/category_closure c2
    where c1.category_id=pg_id and c2.page_id=cat_id
    on duplicate key update page_id=values(page_id);
end //

drop trigger if exists /*_*/insert_category_closure_catlinks //

create trigger /*_*/insert_category_closure_catlinks after insert on /*_*/categorylinks for each row
begin
  declare cat_id int unsigned;
  select page_id from /*_*/page where page_namespace=14 and page_title=NEW.cl_to into cat_id;
  if cat_id is not null then
    call /*_*/do_insert_category_closure_catlinks(NEW.cl_from, cat_id);
  end if;
end //

drop procedure if exists /*_*/do_delete_category_closure_catlinks //

create procedure /*_*/do_delete_category_closure_catlinks(old_page_id int unsigned, old_cat_id int unsigned)
modifies sql data
begin
  declare pg_id int unsigned;
  declare fin int default 0;
  -- rebuild everything to the "left" of deleted edge of categorylinks graph
  declare cur cursor for select page_id
    from /*_*/category_closure where category_id=old_page_id;
  declare continue handler for not found set fin=1;
  open cur;
  repeat
    fetch cur into pg_id;
    delete from /*_*/category_closure where page_id=pg_id;
    call /*_*/fill_category_closure(pg_id);
  until fin end repeat;
  if old_page_id is not null then
    delete from /*_*/category_closure where page_id=old_page_id;
    call /*_*/fill_category_closure(old_page_id);
  end if;
end //

drop trigger if exists /*_*/delete_category_closure_catlinks //

create trigger /*_*/delete_category_closure_catlinks after delete on /*_*/categorylinks for each row
begin
  declare cat_id int unsigned;
  select page_id from /*_*/page where page_namespace=14 and page_title=OLD.cl_to into cat_id;
  if cat_id is not null then
    call /*_*/do_delete_category_closure_catlinks(OLD.cl_from, cat_id);
  end if;
end //

drop trigger if exists /*_*/update_category_closure_catlinks //

create trigger /*_*/update_category_closure_catlinks after update on /*_*/categorylinks for each row
begin
  -- treat update as delete+insert
  declare cat_id int unsigned;
  if OLD.cl_from != NEW.cl_from or OLD.cl_to != NEW.cl_to then
    select page_id from /*_*/page where page_namespace=14 and page_title=OLD.cl_to into cat_id;
    if cat_id is not null then
      call /*_*/do_delete_category_closure_catlinks(OLD.cl_from, cat_id);
    end if;
    select page_id from /*_*/page where page_namespace=14 and page_title=NEW.cl_to into cat_id;
    if cat_id is not null then
      call /*_*/do_insert_category_closure_catlinks(NEW.cl_from, cat_id);
    end if;
  end if;
end //

--
-- Also handle additions/removals of category pages
--

-- page added -> refresh_parent_pages_for_children(), refresh_parent_for_page()
-- page removed -> refresh_parent_pages_for_parent_children()
-- page renamed -> refresh_parent_pages_for_parent_children(), refresh_parent_pages_for_children(), refresh_parent_for_page()

drop trigger if exists /*_*/insert_category_closure_page //

create trigger /*_*/insert_category_closure_page after insert on /*_*/page for each row
begin
  if NEW.page_namespace=14 then
    -- new category is emerging
    insert into /*_*/category_closure (page_id, category_id)
      select cl_from, NEW.page_id
      from /*_*/categorylinks
      where cl_to=NEW.page_title
      on duplicate key update page_id=values(page_id);
    insert into /*_*/category_closure (page_id, category_id)
      select c1.page_id, c2.category_id
      from /*_*/category_closure c1, /*_*/category_closure c2
      where c1.category_id=c2.page_id and c2.category_id=NEW.page_id
      on duplicate key update page_id=values(page_id);
  end if;
  -- add new parent/child subpage records
  call /*_*/refresh_all_parents_for_page(NEW.page_id, NEW.page_namespace, NEW.page_title);
end //

drop trigger if exists /*_*/delete_category_closure_page //

create trigger /*_*/delete_category_closure_page after delete on /*_*/page for each row
begin
  if OLD.page_namespace=14 then
    call /*_*/do_delete_category_closure_catlinks(NULL, OLD.page_id);
  end if;
  call /*_*/refresh_parent_pages_for_parent_children(OLD.page_id, OLD.page_namespace, OLD.page_title);
end //

drop trigger if exists /*_*/update_category_closure_page //

create trigger /*_*/update_category_closure_page after update on /*_*/page for each row
begin
  if (OLD.page_namespace!=NEW.page_namespace or OLD.page_id!=NEW.page_id OR OLD.page_title!=NEW.page_title) then
    -- can normally happen only upon rename of a category page
    -- also treat as delete+insert
    if OLD.page_namespace=14 then
      call /*_*/do_delete_category_closure_catlinks(NULL, OLD.page_id);
    end if;
    if NEW.page_namespace=14 then
      -- new category is emerging
      insert into /*_*/category_closure (page_id, category_id)
        select cl_from, NEW.page_id
        from /*_*/categorylinks
        where cl_to=NEW.page_title
        on duplicate key update page_id=values(page_id);
      insert into /*_*/category_closure (page_id, category_id)
        select c1.page_id, c2.category_id
        from /*_*/category_closure c1, /*_*/category_closure c2
        where c1.category_id=c2.page_id and c2.category_id=NEW.page_id
        on duplicate key update page_id=values(page_id);
    end if;
    -- update parent/child subpage records
    call /*_*/refresh_parent_pages_for_parent_children(OLD.page_id, OLD.page_namespace, OLD.page_title);
    call /*_*/refresh_all_parents_for_page(NEW.page_id, NEW.page_namespace, NEW.page_title);
  end if;
end //

DELIMITER ;
