diff --git a/media/css/seahub.css b/media/css/seahub.css index 7f601ace54..0a1983d7e8 100644 --- a/media/css/seahub.css +++ b/media/css/seahub.css @@ -2632,6 +2632,7 @@ button.sf-dropdown-toggle:focus { .view-link-alert p { display: inline-block; } +.select-white, .perm-add-perm, .user-perm-add-perm, .group-perm-add-perm, diff --git a/media/js/sort_lib.js b/media/js/sort_lib.js deleted file mode 100644 index d051791819..0000000000 --- a/media/js/sort_lib.js +++ /dev/null @@ -1,79 +0,0 @@ -function sort_lib_by_name() { - //include 20902 chinese words, Unicode(19968-40869) - var strChineseFirstPY = "YDYQSXMWZSSXJBYMGCCZQPSSQBYCDSCDQLDYLYBSSJGYZZJJFKCCLZDHWDWZJLJPFYYNWJJTMYHZWZHFLZPPQHGSCYYYNJQYXXGJHHSDSJNKKTMOMLCRXYPSNQSECCQZGGLLYJLMYZZSECYKYYHQWJSSGGYXYZYJWWKDJHYCHMYXJTLXJYQBYXZLDWRDJRWYSRLDZJPCBZJJBRCFTLECZSTZFXXZHTRQHYBDLYCZSSYMMRFMYQZPWWJJYFCRWFDFZQPYDDWYXKYJAWJFFXYPSFTZYHHYZYSWCJYXSCLCXXWZZXNBGNNXBXLZSZSBSGPYSYZDHMDZBQBZCWDZZYYTZHBTSYYBZGNTNXQYWQSKBPHHLXGYBFMJEBJHHGQTJCYSXSTKZHLYCKGLYSMZXYALMELDCCXGZYRJXSDLTYZCQKCNNJWHJTZZCQLJSTSTBNXBTYXCEQXGKWJYFLZQLYHYXSPSFXLMPBYSXXXYDJCZYLLLSJXFHJXPJBTFFYABYXBHZZBJYZLWLCZGGBTSSMDTJZXPTHYQTGLJSCQFZKJZJQNLZWLSLHDZBWJNCJZYZSQQYCQYRZCJJWYBRTWPYFTWEXCSKDZCTBZHYZZYYJXZCFFZZMJYXXSDZZOTTBZLQWFCKSZSXFYRLNYJMBDTHJXSQQCCSBXYYTSYFBXDZTGBCNSLCYZZPSAZYZZSCJCSHZQYDXLBPJLLMQXTYDZXSQJTZPXLCGLQTZWJBHCTSYJSFXYEJJTLBGXSXJMYJQQPFZASYJNTYDJXKJCDJSZCBARTDCLYJQMWNQNCLLLKBYBZZSYHQQLTWLCCXTXLLZNTYLNEWYZYXCZXXGRKRMTCNDNJTSYYSSDQDGHSDBJGHRWRQLYBGLXHLGTGXBQJDZPYJSJYJCTMRNYMGRZJCZGJMZMGXMPRYXKJNYMSGMZJYMKMFXMLDTGFBHCJHKYLPFMDXLQJJSMTQGZSJLQDLDGJYCALCMZCSDJLLNXDJFFFFJCZFMZFFPFKHKGDPSXKTACJDHHZDDCRRCFQYJKQCCWJDXHWJLYLLZGCFCQDSMLZPBJJPLSBCJGGDCKKDEZSQCCKJGCGKDJTJDLZYCXKLQSCGJCLTFPCQCZGWPJDQYZJJBYJHSJDZWGFSJGZKQCCZLLPSPKJGQJHZZLJPLGJGJJTHJJYJZCZMLZLYQBGJWMLJKXZDZNJQSYZMLJLLJKYWXMKJLHSKJGBMCLYYMKXJQLBMLLKMDXXKWYXYSLMLPSJQQJQXYXFJTJDXMXXLLCXQBSYJBGWYMBGGBCYXPJYGPEPFGDJGBHBNSQJYZJKJKHXQFGQZKFHYGKHDKLLSDJQXPQYKYBNQSXQNSZSWHBSXWHXWBZZXDMNSJBSBKBBZKLYLXGWXDRWYQZMYWSJQLCJXXJXKJEQXSCYETLZHLYYYSDZPAQYZCMTLSHTZCFYZYXYLJSDCJQAGYSLCQLYYYSHMRQQKLDXZSCSSSYDYCJYSFSJBFRSSZQSBXXPXJYSDRCKGJLGDKZJZBDKTCSYQPYHSTCLDJDHMXMCGXYZHJDDTMHLTXZXYLYMOHYJCLTYFBQQXPFBDFHHTKSQHZYYWCNXXCRWHOWGYJLEGWDQCWGFJYCSNTMYTOLBYGWQWESJPWNMLRYDZSZTXYQPZGCWXHNGPYXSHMYQJXZTDPPBFYHZHTJYFDZWKGKZBLDNTSXHQEEGZZYLZMMZYJZGXZXKHKSTXNXXWYLYAPSTHXDWHZYMPXAGKYDXBHNHXKDPJNMYHYLPMGOCSLNZHKXXLPZZLBMLSFBHHGYGYYGGBHSCYAQTYWLXTZQCEZYDQDQMMHTKLLSZHLSJZWFYHQSWSCWLQAZYNYTLSXTHAZNKZZSZZLAXXZWWCTGQQTDDYZTCCHYQZFLXPSLZYGPZSZNGLNDQTBDLXGTCTAJDKYWNSYZLJHHZZCWNYYZYWMHYCHHYXHJKZWSXHZYXLYSKQYSPSLYZWMYPPKBYGLKZHTYXAXQSYSHXASMCHKDSCRSWJPWXSGZJLWWSCHSJHSQNHCSEGNDAQTBAALZZMSSTDQJCJKTSCJAXPLGGXHHGXXZCXPDMMHLDGTYBYSJMXHMRCPXXJZCKZXSHMLQXXTTHXWZFKHCCZDYTCJYXQHLXDHYPJQXYLSYYDZOZJNYXQEZYSQYAYXWYPDGXDDXSPPYZNDLTWRHXYDXZZJHTCXMCZLHPYYYYMHZLLHNXMYLLLMDCPPXHMXDKYCYRDLTXJCHHZZXZLCCLYLNZSHZJZZLNNRLWHYQSNJHXYNTTTKYJPYCHHYEGKCTTWLGQRLGGTGTYGYHPYHYLQYQGCWYQKPYYYTTTTLHYHLLTYTTSPLKYZXGZWGPYDSSZZDQXSKCQNMJJZZBXYQMJRTFFBTKHZKBXLJJKDXJTLBWFZPPTKQTZTGPDGNTPJYFALQMKGXBDCLZFHZCLLLLADPMXDJHLCCLGYHDZFGYDDGCYYFGYDXKSSEBDHYKDKDKHNAXXYBPBYYHXZQGAFFQYJXDMLJCSQZLLPCHBSXGJYNDYBYQSPZWJLZKSDDTACTBXZDYZYPJZQSJNKKTKNJDJGYYPGTLFYQKASDNTCYHBLWDZHBBYDWJRYGKZYHEYYFJMSDTYFZJJHGCXPLXHLDWXXJKYTCYKSSSMTWCTTQZLPBSZDZWZXGZAGYKTYWXLHLSPBCLLOQMMZSSLCMBJCSZZKYDCZJGQQDSMCYTZQQLWZQZXSSFPTTFQMDDZDSHDTDWFHTDYZJYQJQKYPBDJYYXTLJHDRQXXXHAYDHRJLKLYTWHLLRLLRCXYLBWSRSZZSYMKZZHHKYHXKSMDSYDYCJPBZBSQLFCXXXNXKXWYWSDZYQOGGQMMYHCDZTTFJYYBGSTTTYBYKJDHKYXBELHTYPJQNFXFDYKZHQKZBYJTZBXHFDXKDASWTAWAJLDYJSFHBLDNNTNQJTJNCHXFJSRFWHZFMDRYJYJWZPDJKZYJYMPCYZNYNXFBYTFYFWYGDBNZZZDNYTXZEMMQBSQEHXFZMBMFLZZSRXYMJGSXWZJSPRYDJSJGXHJJGLJJYNZZJXHGXKYMLPYYYCXYTWQZSWHWLYRJLPXSLSXMFSWWKLCTNXNYNPSJSZHDZEPTXMYYWXYYSYWLXJQZQXZDCLEEELMCPJPCLWBXSQHFWWTFFJTNQJHJQDXHWLBYZNFJLALKYYJLDXHHYCSTYYWNRJYXYWTRMDRQHWQCMFJDYZMHMYYXJWMYZQZXTLMRSPWWCHAQBXYGZYPXYYRRCLMPYMGKSJSZYSRMYJSNXTPLNBAPPYPYLXYYZKYNLDZYJZCZNNLMZHHARQMPGWQTZMXXMLLHGDZXYHXKYXYCJMFFYYHJFSBSSQLXXNDYCANNMTCJCYPRRNYTYQNYYMBMSXNDLYLYSLJRLXYSXQMLLYZLZJJJKYZZCSFBZXXMSTBJGNXYZHLXNMCWSCYZYFZLXBRNNNYLBNRTGZQYSATSWRYHYJZMZDHZGZDWYBSSCSKXSYHYTXXGCQGXZZSHYXJSCRHMKKBXCZJYJYMKQHZJFNBHMQHYSNJNZYBKNQMCLGQHWLZNZSWXKHLJHYYBQLBFCDSXDLDSPFZPSKJYZWZXZDDXJSMMEGJSCSSMGCLXXKYYYLNYPWWWGYDKZJGGGZGGSYCKNJWNJPCXBJJTQTJWDSSPJXZXNZXUMELPXFSXTLLXCLJXJJLJZXCTPSWXLYDHLYQRWHSYCSQYYBYAYWJJJQFWQCQQCJQGXALDBZZYJGKGXPLTZYFXJLTPADKYQHPMATLCPDCKBMTXYBHKLENXDLEEGQDYMSAWHZMLJTWYGXLYQZLJEEYYBQQFFNLYXRDSCTGJGXYYNKLLYQKCCTLHJLQMKKZGCYYGLLLJDZGYDHZWXPYSJBZKDZGYZZHYWYFQYTYZSZYEZZLYMHJJHTSMQWYZLKYYWZCSRKQYTLTDXWCTYJKLWSQZWBDCQYNCJSRSZJLKCDCDTLZZZACQQZZDDXYPLXZBQJYLZLLLQDDZQJYJYJZYXNYYYNYJXKXDAZWYRDLJYYYRJLXLLDYXJCYWYWNQCCLDDNYYYNYCKCZHXXCCLGZQJGKWPPCQQJYSBZZXYJSQPXJPZBSBDSFNSFPZXHDWZTDWPPTFLZZBZDMYYPQJRSDZSQZSQXBDGCPZSWDWCSQZGMDHZXMWWFYBPDGPHTMJTHZSMMBGZMBZJCFZWFZBBZMQCFMBDMCJXLGPNJBBXGYHYYJGPTZGZMQBQTCGYXJXLWZKYDPDYMGCFTPFXYZTZXDZXTGKMTYBBCLBJASKYTSSQYYMSZXFJEWLXLLSZBQJJJAKLYLXLYCCTSXMCWFKKKBSXLLLLJYXTYLTJYYTDPJHNHNNKBYQNFQYYZBYYESSESSGDYHFHWTCJBSDZZTFDMXHCNJZYMQWSRYJDZJQPDQBBSTJGGFBKJBXTGQHNGWJXJGDLLTHZHHYYYYYYSXWTYYYCCBDBPYPZYCCZYJPZYWCBDLFWZCWJDXXHYHLHWZZXJTCZLCDPXUJCZZZLYXJJTXPHFXWPYWXZPTDZZBDZCYHJHMLXBQXSBYLRDTGJRRCTTTHYTCZWMXFYTWWZCWJWXJYWCSKYBZSCCTZQNHXNWXXKHKFHTSWOCCJYBCMPZZYKBNNZPBZHHZDLSYDDYTYFJPXYNGFXBYQXCBHXCPSXTYZDMKYSNXSXLHKMZXLYHDHKWHXXSSKQYHHCJYXGLHZXCSNHEKDTGZXQYPKDHEXTYKCNYMYYYPKQYYYKXZLTHJQTBYQHXBMYHSQCKWWYLLHCYYLNNEQXQWMCFBDCCMLJGGXDQKTLXKGNQCDGZJWYJJLYHHQTTTNWCHMXCXWHWSZJYDJCCDBQCDGDNYXZTHCQRXCBHZTQCBXWGQWYYBXHMBYMYQTYEXMQKYAQYRGYZSLFYKKQHYSSQYSHJGJCNXKZYCXSBXYXHYYLSTYCXQTHYSMGSCPMMGCCCCCMTZTASMGQZJHKLOSQYLSWTMXSYQKDZLJQQYPLSYCZTCQQPBBQJZCLPKHQZYYXXDTDDTSJCXFFLLCHQXMJLWCJCXTSPYCXNDTJSHJWXDQQJSKXYAMYLSJHMLALYKXCYYDMNMDQMXMCZNNCYBZKKYFLMCHCMLHXRCJJHSYLNMTJZGZGYWJXSRXCWJGJQHQZDQJDCJJZKJKGDZQGJJYJYLXZXXCDQHHHEYTMHLFSBDJSYYSHFYSTCZQLPBDRFRZTZYKYWHSZYQKWDQZRKMSYNBCRXQBJYFAZPZZEDZCJYWBCJWHYJBQSZYWRYSZPTDKZPFPBNZTKLQYHBBZPNPPTYZZYBQNYDCPJMMCYCQMCYFZZDCMNLFPBPLNGQJTBTTNJZPZBBZNJKLJQYLNBZQHKSJZNGGQSZZKYXSHPZSNBCGZKDDZQANZHJKDRTLZLSWJLJZLYWTJNDJZJHXYAYNCBGTZCSSQMNJPJYTYSWXZFKWJQTKHTZPLBHSNJZSYZBWZZZZLSYLSBJHDWWQPSLMMFBJDWAQYZTCJTBNNWZXQXCDSLQGDSDPDZHJTQQPSWLYYJZLGYXYZLCTCBJTKTYCZJTQKBSJLGMGZDMCSGPYNJZYQYYKNXRPWSZXMTNCSZZYXYBYHYZAXYWQCJTLLCKJJTJHGDXDXYQYZZBYWDLWQCGLZGJGQRQZCZSSBCRPCSKYDZNXJSQGXSSJMYDNSTZTPBDLTKZWXQWQTZEXNQCZGWEZKSSBYBRTSSSLCCGBPSZQSZLCCGLLLZXHZQTHCZMQGYZQZNMCOCSZJMMZSQPJYGQLJYJPPLDXRGZYXCCSXHSHGTZNLZWZKJCXTCFCJXLBMQBCZZWPQDNHXLJCTHYZLGYLNLSZZPCXDSCQQHJQKSXZPBAJYEMSMJTZDXLCJYRYYNWJBNGZZTMJXLTBSLYRZPYLSSCNXPHLLHYLLQQZQLXYMRSYCXZLMMCZLTZSDWTJJLLNZGGQXPFSKYGYGHBFZPDKMWGHCXMSGDXJMCJZDYCABXJDLNBCDQYGSKYDQTXDJJYXMSZQAZDZFSLQXYJSJZYLBTXXWXQQZBJZUFBBLYLWDSLJHXJYZJWTDJCZFQZQZZDZSXZZQLZCDZFJHYSPYMPQZMLPPLFFXJJNZZYLSJEYQZFPFZKSYWJJJHRDJZZXTXXGLGHYDXCSKYSWMMZCWYBAZBJKSHFHJCXMHFQHYXXYZFTSJYZFXYXPZLCHMZMBXHZZSXYFYMNCWDABAZLXKTCSHHXKXJJZJSTHYGXSXYYHHHJWXKZXSSBZZWHHHCWTZZZPJXSNXQQJGZYZYWLLCWXZFXXYXYHXMKYYSWSQMNLNAYCYSPMJKHWCQHYLAJJMZXHMMCNZHBHXCLXTJPLTXYJHDYYLTTXFSZHYXXSJBJYAYRSMXYPLCKDUYHLXRLNLLSTYZYYQYGYHHSCCSMZCTZQXKYQFPYYRPFFLKQUNTSZLLZMWWTCQQYZWTLLMLMPWMBZSSTZRBPDDTLQJJBXZCSRZQQYGWCSXFWZLXCCRSZDZMCYGGDZQSGTJSWLJMYMMZYHFBJDGYXCCPSHXNZCSBSJYJGJMPPWAFFYFNXHYZXZYLREMZGZCYZSSZDLLJCSQFNXZKPTXZGXJJGFMYYYSNBTYLBNLHPFZDCYFBMGQRRSSSZXYSGTZRNYDZZCDGPJAFJFZKNZBLCZSZPSGCYCJSZLMLRSZBZZLDLSLLYSXSQZQLYXZLSKKBRXBRBZCYCXZZZEEYFGKLZLYYHGZSGZLFJHGTGWKRAAJYZKZQTSSHJJXDCYZUYJLZYRZDQQHGJZXSSZBYKJPBFRTJXLLFQWJHYLQTYMBLPZDXTZYGBDHZZRBGXHWNJTJXLKSCFSMWLSDQYSJTXKZSCFWJLBXFTZLLJZLLQBLSQMQQCGCZFPBPHZCZJLPYYGGDTGWDCFCZQYYYQYSSCLXZSKLZZZGFFCQNWGLHQYZJJCZLQZZYJPJZZBPDCCMHJGXDQDGDLZQMFGPSYTSDYFWWDJZJYSXYYCZCYHZWPBYKXRYLYBHKJKSFXTZJMMCKHLLTNYYMSYXYZPYJQYCSYCWMTJJKQYRHLLQXPSGTLYYCLJSCPXJYZFNMLRGJJTYZBXYZMSJYJHHFZQMSYXRSZCWTLRTQZSSTKXGQKGSPTGCZNJSJCQCXHMXGGZTQYDJKZDLBZSXJLHYQGGGTHQSZPYHJHHGYYGKGGCWJZZYLCZLXQSFTGZSLLLMLJSKCTBLLZZSZMMNYTPZSXQHJCJYQXYZXZQZCPSHKZZYSXCDFGMWQRLLQXRFZTLYSTCTMJCXJJXHJNXTNRZTZFQYHQGLLGCXSZSJDJLJCYDSJTLNYXHSZXCGJZYQPYLFHDJSBPCCZHJJJQZJQDYBSSLLCMYTTMQTBHJQNNYGKYRQYQMZGCJKPDCGMYZHQLLSLLCLMHOLZGDYYFZSLJCQZLYLZQJESHNYLLJXGJXLYSYYYXNBZLJSSZCQQCJYLLZLTJYLLZLLBNYLGQCHXYYXOXCXQKYJXXXYKLXSXXYQXCYKQXQCSGYXXYQXYGYTQOHXHXPYXXXULCYEYCHZZCBWQBBWJQZSCSZSSLZYLKDESJZWMYMCYTSDSXXSCJPQQSQYLYYZYCMDJDZYWCBTJSYDJKCYDDJLBDJJSODZYSYXQQYXDHHGQQYQHDYXWGMMMAJDYBBBPPBCMUUPLJZSMTXERXJMHQNUTPJDCBSSMSSSTKJTSSMMTRCPLZSZMLQDSDMJMQPNQDXCFYNBFSDQXYXHYAYKQYDDLQYYYSSZBYDSLNTFQTZQPZMCHDHCZCWFDXTMYQSPHQYYXSRGJCWTJTZZQMGWJJTJHTQJBBHWZPXXHYQFXXQYWYYHYSCDYDHHQMNMTMWCPBSZPPZZGLMZFOLLCFWHMMSJZTTDHZZYFFYTZZGZYSKYJXQYJZQBHMBZZLYGHGFMSHPZFZSNCLPBQSNJXZSLXXFPMTYJYGBXLLDLXPZJYZJYHHZCYWHJYLSJEXFSZZYWXKZJLUYDTMLYMQJPWXYHXSKTQJEZRPXXZHHMHWQPWQLYJJQJJZSZCPHJLCHHNXJLQWZJHBMZYXBDHHYPZLHLHLGFWLCHYYTLHJXCJMSCPXSTKPNHQXSRTYXXTESYJCTLSSLSTDLLLWWYHDHRJZSFGXTSYCZYNYHTDHWJSLHTZDQDJZXXQHGYLTZPHCSQFCLNJTCLZPFSTPDYNYLGMJLLYCQHYSSHCHYLHQYQTMZYPBYWRFQYKQSYSLZDQJMPXYYSSRHZJNYWTQDFZBWWTWWRXCWHGYHXMKMYYYQMSMZHNGCEPMLQQMTCWCTMMPXJPJJHFXYYZSXZHTYBMSTSYJTTQQQYYLHYNPYQZLCYZHZWSMYLKFJXLWGXYPJYTYSYXYMZCKTTWLKSMZSYLMPWLZWXWQZSSAQSYXYRHSSNTSRAPXCPWCMGDXHXZDZYFJHGZTTSBJHGYZSZYSMYCLLLXBTYXHBBZJKSSDMALXHYCFYGMQYPJYCQXJLLLJGSLZGQLYCJCCZOTYXMTMTTLLWTGPXYMZMKLPSZZZXHKQYSXCTYJZYHXSHYXZKXLZWPSQPYHJWPJPWXQQYLXSDHMRSLZZYZWTTCYXYSZZSHBSCCSTPLWSSCJCHNLCGCHSSPHYLHFHHXJSXYLLNYLSZDHZXYLSXLWZYKCLDYAXZCMDDYSPJTQJZLNWQPSSSWCTSTSZLBLNXSMNYYMJQBQHRZWTYYDCHQLXKPZWBGQYBKFCMZWPZLLYYLSZYDWHXPSBCMLJBSCGBHXLQHYRLJXYSWXWXZSLDFHLSLYNJLZYFLYJYCDRJLFSYZFSLLCQYQFGJYHYXZLYLMSTDJCYHBZLLNWLXXYGYYHSMGDHXXHHLZZJZXCZZZCYQZFNGWPYLCPKPYYPMCLQKDGXZGGWQBDXZZKZFBXXLZXJTPJPTTBYTSZZDWSLCHZHSLTYXHQLHYXXXYYZYSWTXZKHLXZXZPYHGCHKCFSYHUTJRLXFJXPTZTWHPLYXFCRHXSHXKYXXYHZQDXQWULHYHMJTBFLKHTXCWHJFWJCFPQRYQXCYYYQYGRPYWSGSUNGWCHKZDXYFLXXHJJBYZWTSXXNCYJJYMSWZJQRMHXZWFQSYLZJZGBHYNSLBGTTCSYBYXXWXYHXYYXNSQYXMQYWRGYQLXBBZLJSYLPSYTJZYHYZAWLRORJMKSCZJXXXYXCHDYXRYXXJDTSQFXLYLTSFFYXLMTYJMJUYYYXLTZCSXQZQHZXLYYXZHDNBRXXXJCTYHLBRLMBRLLAXKYLLLJLYXXLYCRYLCJTGJCMTLZLLCYZZPZPCYAWHJJFYBDYYZSMPCKZDQYQPBPCJPDCYZMDPBCYYDYCNNPLMTMLRMFMMGWYZBSJGYGSMZQQQZTXMKQWGXLLPJGZBQCDJJJFPKJKCXBLJMSWMDTQJXLDLPPBXCWRCQFBFQJCZAHZGMYKPHYYHZYKNDKZMBPJYXPXYHLFPNYYGXJDBKXNXHJMZJXSTRSTLDXSKZYSYBZXJLXYSLBZYSLHXJPFXPQNBYLLJQKYGZMCYZZYMCCSLCLHZFWFWYXZMWSXTYNXJHPYYMCYSPMHYSMYDYSHQYZCHMJJMZCAAGCFJBBHPLYZYLXXSDJGXDHKXXTXXNBHRMLYJSLTXMRHNLXQJXYZLLYSWQGDLBJHDCGJYQYCMHWFMJYBMBYJYJWYMDPWHXQLDYGPDFXXBCGJSPCKRSSYZJMSLBZZJFLJJJLGXZGYXYXLSZQYXBEXYXHGCXBPLDYHWETTWWCJMBTXCHXYQXLLXFLYXLLJLSSFWDPZSMYJCLMWYTCZPCHQEKCQBWLCQYDPLQPPQZQFJQDJHYMMCXTXDRMJWRHXCJZYLQXDYYNHYYHRSLSRSYWWZJYMTLTLLGTQCJZYABTCKZCJYCCQLJZQXALMZYHYWLWDXZXQDLLQSHGPJFJLJHJABCQZDJGTKHSSTCYJLPSWZLXZXRWGLDLZRLZXTGSLLLLZLYXXWGDZYGBDPHZPBRLWSXQBPFDWOFMWHLYPCBJCCLDMBZPBZZLCYQXLDOMZBLZWPDWYYGDSTTHCSQSCCRSSSYSLFYBFNTYJSZDFNDPDHDZZMBBLSLCMYFFGTJJQWFTMTPJWFNLBZCMMJTGBDZLQLPYFHYYMJYLSDCHDZJWJCCTLJCLDTLJJCPDDSQDSSZYBNDBJLGGJZXSXNLYCYBJXQYCBYLZCFZPPGKCXZDZFZTJJFJSJXZBNZYJQTTYJYHTYCZHYMDJXTTMPXSPLZCDWSLSHXYPZGTFMLCJTYCBPMGDKWYCYZCDSZZYHFLYCTYGWHKJYYLSJCXGYWJCBLLCSNDDBTZBSCLYZCZZSSQDLLMQYYHFSLQLLXFTYHABXGWNYWYYPLLSDLDLLBJCYXJZMLHLJDXYYQYTDLLLBUGBFDFBBQJZZMDPJHGCLGMJJPGAEHHBWCQXAXHHHZCHXYPHJAXHLPHJPGPZJQCQZGJJZZUZDMQYYBZZPHYHYBWHAZYJHYKFGDPFQSDLZMLJXKXGALXZDAGLMDGXMWZQYXXDXXPFDMMSSYMPFMDMMKXKSYZYSHDZKXSYSMMZZZMSYDNZZCZXFPLSTMZDNMXCKJMZTYYMZMZZMSXHHDCZJEMXXKLJSTLWLSQLYJZLLZJSSDPPMHNLZJCZYHMXXHGZCJMDHXTKGRMXFWMCGMWKDTKSXQMMMFZZYDKMSCLCMPCGMHSPXQPZDSSLCXKYXTWLWJYAHZJGZQMCSNXYYMMPMLKJXMHLMLQMXCTKZMJQYSZJSYSZHSYJZJCDAJZYBSDQJZGWZQQXFKDMSDJLFWEHKZQKJPEYPZYSZCDWYJFFMZZYLTTDZZEFMZLBNPPLPLPEPSZALLTYLKCKQZKGENQLWAGYXYDPXLHSXQQWQCQXQCLHYXXMLYCCWLYMQYSKGCHLCJNSZKPYZKCQZQLJPDMDZHLASXLBYDWQLWDNBQCRYDDZTJYBKBWSZDXDTNPJDTCTQDFXQQMGNXECLTTBKPWSLCTYQLPWYZZKLPYGZCQQPLLKCCYLPQMZCZQCLJSLQZDJXLDDHPZQDLJJXZQDXYZQKZLJCYQDYJPPYPQYKJYRMPCBYMCXKLLZLLFQPYLLLMBSGLCYSSLRSYSQTMXYXZQZFDZUYSYZTFFMZZSMZQHZSSCCMLYXWTPZGXZJGZGSJSGKDDHTQGGZLLBJDZLCBCHYXYZHZFYWXYZYMSDBZZYJGTSMTFXQYXQSTDGSLNXDLRYZZLRYYLXQHTXSRTZNGZXBNQQZFMYKMZJBZYMKBPNLYZPBLMCNQYZZZSJZHJCTZKHYZZJRDYZHNPXGLFZTLKGJTCTSSYLLGZRZBBQZZKLPKLCZYSSUYXBJFPNJZZXCDWXZYJXZZDJJKGGRSRJKMSMZJLSJYWQSKYHQJSXPJZZZLSNSHRNYPZTWCHKLPSRZLZXYJQXQKYSJYCZTLQZYBBYBWZPQDWWYZCYTJCJXCKCWDKKZXSGKDZXWWYYJQYYTCYTDLLXWKCZKKLCCLZCQQDZLQLCSFQCHQHSFSMQZZLNBJJZBSJHTSZDYSJQJPDLZCDCWJKJZZLPYCGMZWDJJBSJQZSYZYHHXJPBJYDSSXDZNCGLQMBTSFSBPDZDLZNFGFJGFSMPXJQLMBLGQCYYXBQKDJJQYRFKZTJDHCZKLBSDZCFJTPLLJGXHYXZCSSZZXSTJYGKGCKGYOQXJPLZPBPGTGYJZGHZQZZLBJLSQFZGKQQJZGYCZBZQTLDXRJXBSXXPZXHYZYCLWDXJJHXMFDZPFZHQHQMQGKSLYHTYCGFRZGNQXCLPDLBZCSCZQLLJBLHBZCYPZZPPDYMZZSGYHCKCPZJGSLJLNSCDSLDLXBMSTLDDFJMKDJDHZLZXLSZQPQPGJLLYBDSZGQLBZLSLKYYHZTTNTJYQTZZPSZQZTLLJTYYLLQLLQYZQLBDZLSLYYZYMDFSZSNHLXZNCZQZPBWSKRFBSYZMTHBLGJPMCZZLSTLXSHTCSYZLZBLFEQHLXFLCJLYLJQCBZLZJHHSSTBRMHXZHJZCLXFNBGXGTQJCZTMSFZKJMSSNXLJKBHSJXNTNLZDNTLMSJXGZJYJCZXYJYJWRWWQNZTNFJSZPZSHZJFYRDJSFSZJZBJFZQZZHZLXFYSBZQLZSGYFTZDCSZXZJBQMSZKJRHYJZCKMJKHCHGTXKXQGLXPXFXTRTYLXJXHDTSJXHJZJXZWZLCQSBTXWXGXTXXHXFTSDKFJHZYJFJXRZSDLLLTQSQQZQWZXSYQTWGWBZCGZLLYZBCLMQQTZHZXZXLJFRMYZFLXYSQXXJKXRMQDZDMMYYBSQBHGZMWFWXGMXLZPYYTGZYCCDXYZXYWGSYJYZNBHPZJSQSYXSXRTFYZGRHZTXSZZTHCBFCLSYXZLZQMZLMPLMXZJXSFLBYZMYQHXJSXRXSQZZZSSLYFRCZJRCRXHHZXQYDYHXSJJHZCXZBTYNSYSXJBQLPXZQPYMLXZKYXLXCJLCYSXXZZLXDLLLJJYHZXGYJWKJRWYHCPSGNRZLFZWFZZNSXGXFLZSXZZZBFCSYJDBRJKRDHHGXJLJJTGXJXXSTJTJXLYXQFCSGSWMSBCTLQZZWLZZKXJMLTMJYHSDDBXGZHDLBMYJFRZFSGCLYJBPMLYSMSXLSZJQQHJZFXGFQFQBPXZGYYQXGZTCQWYLTLGWSGWHRLFSFGZJMGMGBGTJFSYZZGZYZAFLSSPMLPFLCWBJZCLJJMZLPJJLYMQDMYYYFBGYGYZMLYZDXQYXRQQQHSYYYQXYLJTYXFSFSLLGNQCYHYCWFHCCCFXPYLYPLLZYXXXXXKQHHXSHJZCFZSCZJXCPZWHHHHHAPYLQALPQAFYHXDYLUKMZQGGGDDESRNNZLTZGCHYPPYSQJJHCLLJTOLNJPZLJLHYMHEYDYDSQYCDDHGZUNDZCLZYZLLZNTNYZGSLHSLPJJBDGWXPCDUTJCKLKCLWKLLCASSTKZZDNQNTTLYYZSSYSSZZRYLJQKCQDHHCRXRZYDGRGCWCGZQFFFPPJFZYNAKRGYWYQPQXXFKJTSZZXSWZDDFBBXTBGTZKZNPZZPZXZPJSZBMQHKCYXYLDKLJNYPKYGHGDZJXXEAHPNZKZTZCMXCXMMJXNKSZQNMNLWBWWXJKYHCPSTMCSQTZJYXTPCTPDTNNPGLLLZSJLSPBLPLQHDTNJNLYYRSZFFJFQWDPHZDWMRZCCLODAXNSSNYZRESTYJWJYJDBCFXNMWTTBYLWSTSZGYBLJPXGLBOCLHPCBJLTMXZLJYLZXCLTPNCLCKXTPZJSWCYXSFYSZDKNTLBYJCYJLLSTGQCBXRYZXBXKLYLHZLQZLNZCXWJZLJZJNCJHXMNZZGJZZXTZJXYCYYCXXJYYXJJXSSSJSTSSTTPPGQTCSXWZDCSYFPTFBFHFBBLZJCLZZDBXGCXLQPXKFZFLSYLTUWBMQJHSZBMDDBCYSCCLDXYCDDQLYJJWMQLLCSGLJJSYFPYYCCYLTJANTJJPWYCMMGQYYSXDXQMZHSZXPFTWWZQSWQRFKJLZJQQYFBRXJHHFWJJZYQAZMYFRHCYYBYQWLPEXCCZSTYRLTTDMQLYKMBBGMYYJPRKZNPBSXYXBHYZDJDNGHPMFSGMWFZMFQMMBCMZZCJJLCNUXYQLMLRYGQZCYXZLWJGCJCGGMCJNFYZZJHYCPRRCMTZQZXHFQGTJXCCJEAQCRJYHPLQLSZDJRBCQHQDYRHYLYXJSYMHZYDWLDFRYHBPYDTSSCNWBXGLPZMLZZTQSSCPJMXXYCSJYTYCGHYCJWYRXXLFEMWJNMKLLSWTXHYYYNCMMCWJDQDJZGLLJWJRKHPZGGFLCCSCZMCBLTBHBQJXQDSPDJZZGKGLFQYWBZYZJLTSTDHQHCTCBCHFLQMPWDSHYYTQWCNZZJTLBYMBPDYYYXSQKXWYYFLXXNCWCXYPMAELYKKJMZZZBRXYYQJFLJPFHHHYTZZXSGQQMHSPGDZQWBWPJHZJDYSCQWZKTXXSQLZYYMYSDZGRXCKKUJLWPYSYSCSYZLRMLQSYLJXBCXTLWDQZPCYCYKPPPNSXFYZJJRCEMHSZMSXLXGLRWGCSTLRSXBZGBZGZTCPLUJLSLYLYMTXMTZPALZXPXJTJWTCYYZLBLXBZLQMYLXPGHDSLSSDMXMBDZZSXWHAMLCZCPJMCNHJYSNSYGCHSKQMZZQDLLKABLWJXSFMOCDXJRRLYQZKJMYBYQLYHETFJZFRFKSRYXFJTWDSXXSYSQJYSLYXWJHSNLXYYXHBHAWHHJZXWMYLJCSSLKYDZTXBZSYFDXGXZJKHSXXYBSSXDPYNZWRPTQZCZENYGCXQFJYKJBZMLJCMQQXUOXSLYXXLYLLJDZBTYMHPFSTTQQWLHOKYBLZZALZXQLHZWRRQHLSTMYPYXJJXMQSJFNBXYXYJXXYQYLTHYLQYFMLKLJTMLLHSZWKZHLJMLHLJKLJSTLQXYLMBHHLNLZXQJHXCFXXLHYHJJGBYZZKBXSCQDJQDSUJZYYHZHHMGSXCSYMXFEBCQWWRBPYYJQTYZCYQYQQZYHMWFFHGZFRJFCDPXNTQYZPDYKHJLFRZXPPXZDBBGZQSTLGDGYLCQMLCHHMFYWLZYXKJLYPQHSYWMQQGQZMLZJNSQXJQSYJYCBEHSXFSZPXZWFLLBCYYJDYTDTHWZSFJMQQYJLMQXXLLDTTKHHYBFPWTYYSQQWNQWLGWDEBZWCMYGCULKJXTMXMYJSXHYBRWFYMWFRXYQMXYSZTZZTFYKMLDHQDXWYYNLCRYJBLPSXCXYWLSPRRJWXHQYPHTYDNXHHMMYWYTZCSQMTSSCCDALWZTCPQPYJLLQZYJSWXMZZMMYLMXCLMXCZMXMZSQTZPPQQBLPGXQZHFLJJHYTJSRXWZXSCCDLXTYJDCQJXSLQYCLZXLZZXMXQRJMHRHZJBHMFLJLMLCLQNLDXZLLLPYPSYJYSXCQQDCMQJZZXHNPNXZMEKMXHYKYQLXSXTXJYYHWDCWDZHQYYBGYBCYSCFGPSJNZDYZZJZXRZRQJJYMCANYRJTLDPPYZBSTJKXXZYPFDWFGZZRPYMTNGXZQBYXNBUFNQKRJQZMJEGRZGYCLKXZDSKKNSXKCLJSPJYYZLQQJYBZSSQLLLKJXTBKTYLCCDDBLSPPFYLGYDTZJYQGGKQTTFZXBDKTYYHYBBFYTYYBCLPDYTGDHRYRNJSPTCSNYJQHKLLLZSLYDXXWBCJQSPXBPJZJCJDZFFXXBRMLAZHCSNDLBJDSZBLPRZTSWSBXBCLLXXLZDJZSJPYLYXXYFTFFFBHJJXGBYXJPMMMPSSJZJMTLYZJXSWXTYLEDQPJMYGQZJGDJLQJWJQLLSJGJGYGMSCLJJXDTYGJQJQJCJZCJGDZZSXQGSJGGCXHQXSNQLZZBXHSGZXCXYLJXYXYYDFQQJHJFXDHCTXJYRXYSQTJXYEFYYSSYYJXNCYZXFXMSYSZXYYSCHSHXZZZGZZZGFJDLTYLNPZGYJYZYYQZPBXQBDZTZCZYXXYHHSQXSHDHGQHJHGYWSZTMZMLHYXGEBTYLZKQWYTJZRCLEKYSTDBCYKQQSAYXCJXWWGSBHJYZYDHCSJKQCXSWXFLTYNYZPZCCZJQTZWJQDZZZQZLJJXLSBHPYXXPSXSHHEZTXFPTLQYZZXHYTXNCFZYYHXGNXMYWXTZSJPTHHGYMXMXQZXTSBCZYJYXXTYYZYPCQLMMSZMJZZLLZXGXZAAJZYXJMZXWDXZSXZDZXLEYJJZQBHZWZZZQTZPSXZTDSXJJJZNYAZPHXYYSRNQDTHZHYYKYJHDZXZLSWCLYBZYECWCYCRYLCXNHZYDZYDYJDFRJJHTRSQTXYXJRJHOJYNXELXSFSFJZGHPZSXZSZDZCQZBYYKLSGSJHCZSHDGQGXYZGXCHXZJWYQWGYHKSSEQZZNDZFKWYSSTCLZSTSYMCDHJXXYWEYXCZAYDMPXMDSXYBSQMJMZJMTZQLPJYQZCGQHXJHHLXXHLHDLDJQCLDWBSXFZZYYSCHTYTYYBHECXHYKGJPXHHYZJFXHWHBDZFYZBCAPNPGNYDMSXHMMMMAMYNBYJTMPXYYMCTHJBZYFCGTYHWPHFTWZZEZSBZEGPFMTSKFTYCMHFLLHGPZJXZJGZJYXZSBBQSCZZLZCCSTPGXMJSFTCCZJZDJXCYBZLFCJSYZFGSZLYBCWZZBYZDZYPSWYJZXZBDSYUXLZZBZFYGCZXBZHZFTPBGZGEJBSTGKDMFHYZZJHZLLZZGJQZLSFDJSSCBZGPDLFZFZSZYZYZSYGCXSNXXCHCZXTZZLJFZGQSQYXZJQDCCZTQCDXZJYQJQCHXZTDLGSCXZSYQJQTZWLQDQZTQCHQQJZYEZZZPBWKDJFCJPZTYPQYQTTYNLMBDKTJZPQZQZZFPZSBNJLGYJDXJDZZKZGQKXDLPZJTCJDQBXDJQJSTCKNXBXZMSLYJCQMTJQWWCJQNJNLLLHJCWQTBZQYDZCZPZZDZYDDCYZZZCCJTTJFZDPRRTZTJDCQTQZDTJNPLZBCLLCTZSXKJZQZPZLBZRBTJDCXFCZDBCCJJLTQQPLDCGZDBBZJCQDCJWYNLLZYZCCDWLLXWZLXRXNTQQCZXKQLSGDFQTDDGLRLAJJTKUYMKQLLTZYTDYYCZGJWYXDXFRSKSTQTENQMRKQZHHQKDLDAZFKYPBGGPZREBZZYKZZSPEGJXGYKQZZZSLYSYYYZWFQZYLZZLZHWCHKYPQGNPGBLPLRRJYXCCSYYHSFZFYBZYYTGZXYLXCZWXXZJZBLFFLGSKHYJZEYJHLPLLLLCZGXDRZELRHGKLZZYHZLYQSZZJZQLJZFLNBHGWLCZCFJYSPYXZLZLXGCCPZBLLCYBBBBUBBCBPCRNNZCZYRBFSRLDCGQYYQXYGMQZWTZYTYJXYFWTEHZZJYWLCCNTZYJJZDEDPZDZTSYQJHDYMBJNYJZLXTSSTPHNDJXXBYXQTZQDDTJTDYYTGWSCSZQFLSHLGLBCZPHDLYZJYCKWTYTYLBNYTSDSYCCTYSZYYEBHEXHQDTWNYGYCLXTSZYSTQMYGZAZCCSZZDSLZCLZRQXYYELJSBYMXSXZTEMBBLLYYLLYTDQYSHYMRQWKFKBFXNXSBYCHXBWJYHTQBPBSBWDZYLKGZSKYHXQZJXHXJXGNLJKZLYYCDXLFYFGHLJGJYBXQLYBXQPQGZTZPLNCYPXDJYQYDYMRBESJYYHKXXSTMXRCZZYWXYQYBMCLLYZHQYZWQXDBXBZWZMSLPDMYSKFMZKLZCYQYCZLQXFZZYDQZPZYGYJYZMZXDZFYFYTTQTZHGSPCZMLCCYTZXJCYTJMKSLPZHYSNZLLYTPZCTZZCKTXDHXXTQCYFKSMQCCYYAZHTJPCYLZLYJBJXTPNYLJYYNRXSYLMMNXJSMYBCSYSYLZYLXJJQYLDZLPQBFZZBLFNDXQKCZFYWHGQMRDSXYCYTXNQQJZYYPFZXDYZFPRXEJDGYQBXRCNFYYQPGHYJDYZXGRHTKYLNWDZNTSMPKLBTHBPYSZBZTJZSZZJTYYXZPHSSZZBZCZPTQFZMYFLYPYBBJQXZMXXDJMTSYSKKBJZXHJCKLPSMKYJZCXTMLJYXRZZQSLXXQPYZXMKYXXXJCLJPRMYYGADYSKQLSNDHYZKQXZYZTCGHZTLMLWZYBWSYCTBHJHJFCWZTXWYTKZLXQSHLYJZJXTMPLPYCGLTBZZTLZJCYJGDTCLKLPLLQPJMZPAPXYZLKKTKDZCZZBNZDYDYQZJYJGMCTXLTGXSZLMLHBGLKFWNWZHDXUHLFMKYSLGXDTWWFRJEJZTZHYDXYKSHWFZCQSHKTMQQHTZHYMJDJSKHXZJZBZZXYMPAGQMSTPXLSKLZYNWRTSQLSZBPSPSGZWYHTLKSSSWHZZLYYTNXJGMJSZSUFWNLSOZTXGXLSAMMLBWLDSZYLAKQCQCTMYCFJBSLXCLZZCLXXKSBZQCLHJPSQPLSXXCKSLNHPSFQQYTXYJZLQLDXZQJZDYYDJNZPTUZDSKJFSLJHYLZSQZLBTXYDGTQFDBYAZXDZHZJNHHQBYKNXJJQCZMLLJZKSPLDYCLBBLXKLELXJLBQYCXJXGCNLCQPLZLZYJTZLJGYZDZPLTQCSXFDMNYCXGBTJDCZNBGBQYQJWGKFHTNPYQZQGBKPBBYZMTJDYTBLSQMPSXTBNPDXKLEMYYCJYNZCTLDYKZZXDDXHQSHDGMZSJYCCTAYRZLPYLTLKXSLZCGGEXCLFXLKJRTLQJAQZNCMBYDKKCXGLCZJZXJHPTDJJMZQYKQSECQZDSHHADMLZFMMZBGNTJNNLGBYJBRBTMLBYJDZXLCJLPLDLPCQDHLXZLYCBLCXZZJADJLNZMMSSSMYBHBSQKBHRSXXJMXSDZNZPXLGBRHWGGFCXGMSKLLTSJYYCQLTSKYWYYHYWXBXQYWPYWYKQLSQPTNTKHQCWDQKTWPXXHCPTHTWUMSSYHBWCRWXHJMKMZNGWTMLKFGHKJYLSYYCXWHYECLQHKQHTTQKHFZLDXQWYZYYDESBPKYRZPJFYYZJCEQDZZDLATZBBFJLLCXDLMJSSXEGYGSJQXCWBXSSZPDYZCXDNYXPPZYDLYJCZPLTXLSXYZYRXCYYYDYLWWNZSAHJSYQYHGYWWAXTJZDAXYSRLTDPSSYYFNEJDXYZHLXLLLZQZSJNYQYQQXYJGHZGZCYJCHZLYCDSHWSHJZYJXCLLNXZJJYYXNFXMWFPYLCYLLABWDDHWDXJMCXZTZPMLQZHSFHZYNZTLLDYWLSLXHYMMYLMBWWKYXYADTXYLLDJPYBPWUXJMWMLLSAFDLLYFLBHHHBQQLTZJCQJLDJTFFKMMMBYTHYGDCQRDDWRQJXNBYSNWZDBYYTBJHPYBYTTJXAAHGQDQTMYSTQXKBTZPKJLZRBEQQSSMJJBDJOTGTBXPGBKTLHQXJJJCTHXQDWJLWRFWQGWSHCKRYSWGFTGYGBXSDWDWRFHWYTJJXXXJYZYSLPYYYPAYXHYDQKXSHXYXGSKQHYWFDDDPPLCJLQQEEWXKSYYKDYPLTJTHKJLTCYYHHJTTPLTZZCDLTHQKZXQYSTEEYWYYZYXXYYSTTJKLLPZMCYHQGXYHSRMBXPLLNQYDQHXSXXWGDQBSHYLLPJJJTHYJKYPPTHYYKTYEZYENMDSHLCRPQFDGFXZPSFTLJXXJBSWYYSKSFLXLPPLBBBLBSFXFYZBSJSSYLPBBFFFFSSCJDSTZSXZRYYSYFFSYZYZBJTBCTSBSDHRTJJBYTCXYJEYLXCBNEBJDSYXYKGSJZBXBYTFZWGENYHHTHZHHXFWGCSTBGXKLSXYWMTMBYXJSTZSCDYQRCYTWXZFHMYMCXLZNSDJTTTXRYCFYJSBSDYERXJLJXBBDEYNJGHXGCKGSCYMBLXJMSZNSKGXFBNBPTHFJAAFXYXFPXMYPQDTZCXZZPXRSYWZDLYBBKTYQPQJPZYPZJZNJPZJLZZFYSBTTSLMPTZRTDXQSJEHBZYLZDHLJSQMLHTXTJECXSLZZSPKTLZKQQYFSYGYWPCPQFHQHYTQXZKRSGTTSQCZLPTXCDYYZXSQZSLXLZMYCPCQBZYXHBSXLZDLTCDXTYLZJYYZPZYZLTXJSJXHLPMYTXCQRBLZSSFJZZTNJYTXMYJHLHPPLCYXQJQQKZZSCPZKSWALQSBLCCZJSXGWWWYGYKTJBBZTDKHXHKGTGPBKQYSLPXPJCKBMLLXDZSTBKLGGQKQLSBKKTFXRMDKBFTPZFRTBBRFERQGXYJPZSSTLBZTPSZQZSJDHLJQLZBPMSMMSXLQQNHKNBLRDDNXXDHDDJCYYGYLXGZLXSYGMQQGKHBPMXYXLYTQWLWGCPBMQXCYZYDRJBHTDJYHQSHTMJSBYPLWHLZFFNYPMHXXHPLTBQPFBJWQDBYGPNZTPFZJGSDDTQSHZEAWZZYLLTYYBWJKXXGHLFKXDJTMSZSQYNZGGSWQSPHTLSSKMCLZXYSZQZXNCJDQGZDLFNYKLJCJLLZLMZZNHYDSSHTHZZLZZBBHQZWWYCRZHLYQQJBEYFXXXWHSRXWQHWPSLMSSKZTTYGYQQWRSLALHMJTQJSMXQBJJZJXZYZKXBYQXBJXSHZTSFJLXMXZXFGHKZSZGGYLCLSARJYHSLLLMZXELGLXYDJYTLFBHBPNLYZFBBHPTGJKWETZHKJJXZXXGLLJLSTGSHJJYQLQZFKCGNNDJSSZFDBCTWWSEQFHQJBSAQTGYPQLBXBMMYWXGSLZHGLZGQYFLZBYFZJFRYSFMBYZHQGFWZSYFYJJPHZBYYZFFWODGRLMFTWLBZGYCQXCDJYGZYYYYTYTYDWEGAZYHXJLZYYHLRMGRXXZCLHNELJJTJTPWJYBJJBXJJTJTEEKHWSLJPLPSFYZPQQBDLQJJTYYQLYZKDKSQJYYQZLDQTGJQYZJSUCMRYQTHTEJMFCTYHYPKMHYZWJDQFHYYXWSHCTXRLJHQXHCCYYYJLTKTTYTMXGTCJTZAYYOCZLYLBSZYWJYTSJYHBYSHFJLYGJXXTMZYYLTXXYPZLXYJZYZYYPNHMYMDYYLBLHLSYYQQLLNJJYMSOYQBZGDLYXYLCQYXTSZEGXHZGLHWBLJHEYXTWQMAKBPQCGYSHHEGQCMWYYWLJYJHYYZLLJJYLHZYHMGSLJLJXCJJYCLYCJPCPZJZJMMYLCQLNQLJQJSXYJMLSZLJQLYCMMHCFMMFPQQMFYLQMCFFQMMMMHMZNFHHJGTTHHKHSLNCHHYQDXTMMQDCYZYXYQMYQYLTDCYYYZAZZCYMZYDLZFFFMMYCQZWZZMABTBYZTDMNZZGGDFTYPCGQYTTSSFFWFDTZQSSYSTWXJHXYTSXXYLBYQHWWKXHZXWZNNZZJZJJQJCCCHYYXBZXZCYZTLLCQXYNJYCYYCYNZZQYYYEWYCZDCJYCCHYJLBTZYYCQWMPWPYMLGKDLDLGKQQBGYCHJXY"; - var id = $(this).attr('id'), by, other, - table = $(this).closest('table'), - repo_items = $('tr:gt(0)', table); - - if (id.indexOf('up') != -1) { - by = function(a, b) { return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1 }; - other = $('#' + id.replace('up', 'down')); - } else { - by = function(a, b) { return a.name.toLowerCase() < b.name.toLowerCase() ? 1 : -1 }; - other = $('#' + id.replace('down', 'up')); - } - var name_list = [], cn_name_list = []; - repo_items.each(function() { - var name = $(this).attr('data-repo_name'); - //get unicode - var uni = name.charCodeAt(0); - //not a Chinese character - if (uni > 40869 || uni < 19968) { - name_list.push({'name': name, 'element': this}); - } else { - //get Chinese character's PinYin - cn_name_list.push({'name': strChineseFirstPY.charAt(uni - 19968), 'element': this}); - } - }); - - name_list.sort(by); - cn_name_list.sort(by); - repo_items.detach(); - - if (id.indexOf('up') != -1) { - $(name_list).each(function(index, item) { - table.append(item.element); - }); - $(cn_name_list).each(function(index, item) { - table.append(item.element); - }); - } else { - $(cn_name_list).each(function(index, item) { - table.append(item.element); - }); - $(name_list).each(function(index, item) { - table.append(item.element); - }); - } - $(this).addClass('hide'); - other.removeClass('hide'); -}; - -function sort_lib_by_time() { - var id = $(this).attr('id'), by, other, sort_list = [], - table = $(this).closest('table'), - repo_items = $('tr:gt(0)', table); - - if (id.indexOf('up') != -1) { - by = function(a, b) { return a.time < b.time ? -1 : 1 }; - other = $('#' + id.replace('up', 'down')); - } else { - by = function(a, b) { return a.time < b.time ? 1 : -1 }; - other = $('#' + id.replace('down', 'up')); - } - - repo_items.each(function() { - sort_list.push({'time':$(this).data('time'), 'element':this}); - }); - - sort_list.sort(by); - - repo_items.detach(); - - $(sort_list).each(function(index, item) { - table.append(item.element); - }); - - $(this).addClass('hide'); - other.removeClass('hide'); -} diff --git a/seahub/api2/endpoints/dir_shared_items.py b/seahub/api2/endpoints/dir_shared_items.py index 1253e285f3..0bb0bf3e96 100644 --- a/seahub/api2/endpoints/dir_shared_items.py +++ b/seahub/api2/endpoints/dir_shared_items.py @@ -423,6 +423,10 @@ class DirSharedItemsEndpoint(APIView): if shared_to is None or not is_valid_username(shared_to): return api_error(status.HTTP_400_BAD_REQUEST, 'Email %s invalid.' % shared_to) + # if user not found, permission will be None + permission = seafile_api.check_permission_by_path( + shared_repo.id, '/', shared_to) + if is_org_context(request): org_id = request.user.org.org_id seaserv.seafserv_threaded_rpc.org_remove_share( @@ -430,9 +434,6 @@ class DirSharedItemsEndpoint(APIView): else: seaserv.remove_share(shared_repo.id, username, shared_to) - # if user not found, permission will be None - permission = seafile_api.check_permission_by_path(repo.id, path, - shared_to) send_perm_audit_msg('delete-repo-perm', username, shared_to, repo_id, path, permission) @@ -445,7 +446,15 @@ class DirSharedItemsEndpoint(APIView): # hacky way to get group repo permission permission = '' - for e in seafile_api.list_repo_shared_group_by_user(username, shared_repo.id): + if is_org_context(request): + org_id = request.user.org.org_id + shared_groups = seafile_api.list_org_repo_shared_group( + org_id, username, shared_repo.id) + else: + shared_groups = seafile_api.list_repo_shared_group( + username, shared_repo.id) + + for e in shared_groups: if e.group_id == group_id: permission = e.perm break diff --git a/seahub/api2/endpoints/share_links.py b/seahub/api2/endpoints/share_links.py index 7e2b775fed..4cf510ad9b 100644 --- a/seahub/api2/endpoints/share_links.py +++ b/seahub/api2/endpoints/share_links.py @@ -1,3 +1,4 @@ +import os import logging from constance import config from dateutil.relativedelta import relativedelta @@ -21,6 +22,7 @@ from seahub.api2.throttling import UserRateThrottle from seahub.share.models import FileShare, OrgFileShare from seahub.utils import gen_shared_link, is_org_context from seahub.views import check_folder_permission +from seahub.utils.timeutils import datetime_to_isoformat_timestr logger = logging.getLogger(__name__) @@ -29,15 +31,43 @@ def get_share_link_info(fileshare): data = {} token = fileshare.token - data['repo_id'] = fileshare.repo_id - data['path'] = fileshare.path - data['ctime'] = fileshare.ctime - data['view_cnt'] = fileshare.view_cnt - data['link'] = gen_shared_link(token, fileshare.s_type) - data['token'] = token - data['expire_date'] = fileshare.expire_date - data['is_expired'] = fileshare.is_expired() + repo_id = fileshare.repo_id + try: + repo = seafile_api.get_repo(repo_id) + except Exception as e: + logger.error(e) + repo = None + + path = fileshare.path + if path: + obj_name = '/' if path == '/' else os.path.basename(path.rstrip('/')) + else: + obj_name = '' + + if fileshare.expire_date: + expire_date = datetime_to_isoformat_timestr(fileshare.expire_date) + else: + expire_date = '' + + if fileshare.ctime: + ctime = datetime_to_isoformat_timestr(fileshare.ctime) + else: + ctime = '' + data['username'] = fileshare.username + data['repo_id'] = repo_id + data['repo_name'] = repo.repo_name if repo else '' + + data['path'] = path + data['obj_name'] = obj_name + data['is_dir'] = True if fileshare.s_type == 'd' else False + + data['token'] = token + data['link'] = gen_shared_link(token, fileshare.s_type) + data['view_cnt'] = fileshare.view_cnt + data['ctime'] = ctime + data['expire_date'] = expire_date + data['is_expired'] = fileshare.is_expired() return data @@ -64,14 +94,20 @@ class ShareLinks(APIView): return (None, None) def get(self, request): - """ get share links. + """ Get all share links of a user. + + Permission checking: + 1. default(NOT guest) user; """ if not self._can_generate_shared_link(request): error_msg = 'Permission denied.' return api_error(status.HTTP_403_FORBIDDEN, error_msg) - # check if args invalid + # get all share links + username = request.user.username + fileshares = FileShare.objects.filter(username=username) + repo_id = request.GET.get('repo_id', None) if repo_id: repo = seafile_api.get_repo(repo_id) @@ -79,81 +115,91 @@ class ShareLinks(APIView): error_msg = 'Library %s not found.' % repo_id return api_error(status.HTTP_404_NOT_FOUND, error_msg) - # repo level permission check - if not check_folder_permission(request, repo_id, '/'): - error_msg = 'Permission denied.' - return api_error(status.HTTP_403_FORBIDDEN, error_msg) - - path = request.GET.get('path', None) - if path: - try: - obj_id, s_type = self._generate_obj_id_and_type_by_path(repo_id, path) - except SearpcError as e: - logger.error(e) - error_msg = 'Internal Server Error' - return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) - - if not obj_id: - if s_type == 'f': - error_msg = 'file %s not found.' % path - elif s_type == 'd': - error_msg = 'folder %s not found.' % path - else: - error_msg = 'path %s not found.' % path - - return api_error(status.HTTP_404_NOT_FOUND, error_msg) - - # folder/path permission check - if not check_folder_permission(request, repo_id, path): - error_msg = 'Permission denied.' - return api_error(status.HTTP_403_FORBIDDEN, error_msg) - - username = request.user.username - fileshares = FileShare.objects.filter(username=username) - - # filter result by args - if repo_id: + # filter share links by repo fileshares = filter(lambda fs: fs.repo_id == repo_id, fileshares) - if path: - if s_type == 'd' and path[-1] != '/': - path = path + '/' + path = request.GET.get('path', None) + if path: + try: + obj_id, s_type = self._generate_obj_id_and_type_by_path(repo_id, path) + except SearpcError as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) - fileshares = filter(lambda fs: fs.path == path, fileshares) + if not obj_id: + if s_type == 'f': + error_msg = 'file %s not found.' % path + elif s_type == 'd': + error_msg = 'folder %s not found.' % path + else: + error_msg = 'path %s not found.' % path - result = [] + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # if path invalid, filter share links by repo + if s_type == 'd' and path[-1] != '/': + path = path + '/' + + fileshares = filter(lambda fs: fs.path == path, fileshares) + + links_info = [] for fs in fileshares: link_info = get_share_link_info(fs) - result.append(link_info) + links_info.append(link_info) - if len(result) == 1: - result = result[0] + if len(links_info) == 1: + result = links_info + else: + dir_list = filter(lambda x: x['is_dir'], links_info) + file_list = filter(lambda x: not x['is_dir'], links_info) + + dir_list.sort(lambda x, y: cmp(x['obj_name'], y['obj_name'])) + file_list.sort(lambda x, y: cmp(x['obj_name'], y['obj_name'])) + + result = dir_list + file_list return Response(result) def post(self, request): - """ create share link. + """ Create share link. + + Permission checking: + 1. default(NOT guest) user; """ - if not self._can_generate_shared_link(request): - error_msg = 'Permission denied.' - return api_error(status.HTTP_403_FORBIDDEN, error_msg) - + # argument check repo_id = request.data.get('repo_id', None) if not repo_id: error_msg = 'repo_id invalid.' return api_error(status.HTTP_400_BAD_REQUEST, error_msg) - repo = seafile_api.get_repo(repo_id) - if not repo: - error_msg = 'Library %s not found.' % repo_id - return api_error(status.HTTP_404_NOT_FOUND, error_msg) - path = request.data.get('path', None) if not path: error_msg = 'path invalid.' return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + password = request.data.get('password', None) + if password and len(password) < config.SHARE_LINK_PASSWORD_MIN_LENGTH: + error_msg = _('Password is too short.') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + expire_days = int(request.data.get('expire_days', 0)) + except ValueError: + expire_days = 0 + + if expire_days <= 0: + expire_date = None + else: + expire_date = timezone.now() + relativedelta(days=expire_days) + + # resource check + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + try: obj_id, s_type = self._generate_obj_id_and_type_by_path(repo_id, path) except SearpcError as e: @@ -172,24 +218,13 @@ class ShareLinks(APIView): return api_error(status.HTTP_404_NOT_FOUND, error_msg) # permission check - if not check_folder_permission(request, repo_id, path): + if not self._can_generate_shared_link(request): error_msg = 'Permission denied.' return api_error(status.HTTP_403_FORBIDDEN, error_msg) - password = request.data.get('password', None) - if password and len(password) < config.SHARE_LINK_PASSWORD_MIN_LENGTH: - error_msg = _('Password is too short.') - return api_error(status.HTTP_400_BAD_REQUEST, error_msg) - - try: - expire_days = int(request.data.get('expire_days', 0)) - except ValueError: - expire_days = 0 - - if expire_days <= 0: - expire_date = None - else: - expire_date = timezone.now() + relativedelta(days=expire_days) + if not check_folder_permission(request, repo_id, path): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) username = request.user.username if s_type == 'f': @@ -197,18 +232,16 @@ class ShareLinks(APIView): if not fs: fs = FileShare.objects.create_file_link(username, repo_id, path, password, expire_date) - if is_org_context(request): - org_id = request.user.org.org_id - OrgFileShare.objects.set_org_file_share(org_id, fs) elif s_type == 'd': fs = FileShare.objects.get_dir_link_by_path(username, repo_id, path) if not fs: fs = FileShare.objects.create_dir_link(username, repo_id, path, - password, expire_date) - if is_org_context(request): - org_id = request.user.org.org_id - OrgFileShare.objects.set_org_file_share(org_id, fs) + password, expire_date) + + if is_org_context(request): + org_id = request.user.org.org_id + OrgFileShare.objects.set_org_file_share(org_id, fs) link_info = get_share_link_info(fs) return Response(link_info) @@ -217,24 +250,17 @@ class ShareLink(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) permission_classes = (IsAuthenticated,) - throttle_classes = (UserRateThrottle, ) + throttle_classes = (UserRateThrottle,) def _can_generate_shared_link(self, request): return request.user.permissions.can_generate_shared_link() def get(self, request, token): - try: - fs = FileShare.objects.get(token=token) - except FileShare.DoesNotExist: - error_msg = 'token %s not found.' % token - return api_error(status.HTTP_404_NOT_FOUND, error_msg) + """ Get a special share link info. - link_info = get_share_link_info(fs) - return Response(link_info) - - def delete(self, request, token): - """ delete share link. + Permission checking: + 1. default(NOT guest) user; """ if not self._can_generate_shared_link(request): @@ -247,6 +273,26 @@ class ShareLink(APIView): error_msg = 'token %s not found.' % token return api_error(status.HTTP_404_NOT_FOUND, error_msg) + link_info = get_share_link_info(fs) + return Response(link_info) + + def delete(self, request, token): + """ Delete share link. + + Permission checking: + 1. default(NOT guest) user; + 2. link owner; + """ + + if not self._can_generate_shared_link(request): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + fs = FileShare.objects.get(token=token) + except FileShare.DoesNotExist: + return Response({'success': True}) + username = request.user.username if not fs.is_owner(username): error_msg = 'Permission denied.' @@ -254,8 +300,9 @@ class ShareLink(APIView): try: fs.delete() - return Response({'success': True}) except Exception as e: logger.error(e) error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + return Response({'success': True}) diff --git a/seahub/api2/endpoints/shared_folders.py b/seahub/api2/endpoints/shared_folders.py new file mode 100644 index 0000000000..31f6f693e6 --- /dev/null +++ b/seahub/api2/endpoints/shared_folders.py @@ -0,0 +1,77 @@ +import logging + +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status + +import seaserv +from seaserv import seafile_api, ccnet_api + +from seahub.api2.utils import api_error +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.throttling import UserRateThrottle + +from seahub.utils import is_org_context +from seahub.base.templatetags.seahub_tags import email2nickname + +logger = logging.getLogger(__name__) + +class SharedFolders(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def get(self, request, format=None): + """ List all shared out folders. + + Permission checking: + 1. all authenticated user can perform this action. + """ + + shared_repos = [] + username = request.user.username + + try: + if is_org_context(request): + org_id = request.user.org.org_id + shared_repos += seafile_api.get_org_share_out_repo_list(org_id, username, -1, -1) + shared_repos += seaserv.seafserv_threaded_rpc.get_org_group_repos_by_owner(org_id, username) + #shared_repos += seaserv.seafserv_threaded_rpc.list_org_inner_pub_repos_by_owner(org_id, username) + else: + shared_repos += seafile_api.get_share_out_repo_list(username, -1, -1) + shared_repos += seafile_api.get_group_repos_by_owner(username) + #if not request.cloud_mode: + #shared_repos += seaserv.list_inner_pub_repos_by_owner(username) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + returned_result = [] + shared_repos.sort(lambda x, y: cmp(x.repo_name, y.repo_name)) + for repo in shared_repos: + if not repo.is_virtual: + continue + + result = {} + result['repo_id'] = repo.origin_repo_id + result['path'] = repo.origin_path + result['folder_name'] = repo.name + result['share_type'] = repo.share_type + result['share_permission'] = repo.permission + + if repo.share_type == 'personal': + result['user_name'] = email2nickname(repo.user) + result['user_email'] = repo.user + + if repo.share_type == 'group': + group = ccnet_api.get_group(repo.group_id) + result['group_id'] = repo.group_id + result['group_name'] = group.group_name + + returned_result.append(result) + + return Response(returned_result) diff --git a/seahub/api2/endpoints/shared_repos.py b/seahub/api2/endpoints/shared_repos.py new file mode 100644 index 0000000000..50cb081a6d --- /dev/null +++ b/seahub/api2/endpoints/shared_repos.py @@ -0,0 +1,324 @@ +import logging + +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status + +import seaserv +from seaserv import seafile_api, ccnet_api + +from seahub.api2.utils import api_error +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.throttling import UserRateThrottle + +from seahub.utils import is_org_context, is_valid_username, send_perm_audit_msg +from seahub.base.templatetags.seahub_tags import email2nickname + +logger = logging.getLogger(__name__) + +class SharedRepos(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def get(self, request, format=None): + """ List all shared out repos. + + Permission checking: + 1. all authenticated user can perform this action. + """ + + shared_repos = [] + username = request.user.username + try: + if is_org_context(request): + org_id = request.user.org.org_id + shared_repos += seafile_api.get_org_share_out_repo_list(org_id, username, -1, -1) + shared_repos += seaserv.seafserv_threaded_rpc.get_org_group_repos_by_owner(org_id, username) + shared_repos += seaserv.seafserv_threaded_rpc.list_org_inner_pub_repos_by_owner(org_id, username) + else: + shared_repos += seafile_api.get_share_out_repo_list(username, -1, -1) + shared_repos += seafile_api.get_group_repos_by_owner(username) + if not request.cloud_mode: + shared_repos += seaserv.list_inner_pub_repos_by_owner(username) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + returned_result = [] + shared_repos.sort(lambda x, y: cmp(x.repo_name, y.repo_name)) + for repo in shared_repos: + if repo.is_virtual: + continue + + result = {} + result['repo_id'] = repo.repo_id + result['repo_name'] = repo.repo_name + result['share_type'] = repo.share_type + result['share_permission'] = repo.permission + + if repo.share_type == 'personal': + result['user_name'] = email2nickname(repo.user) + result['user_email'] = repo.user + + if repo.share_type == 'group': + group = ccnet_api.get_group(repo.group_id) + result['group_id'] = repo.group_id + result['group_name'] = group.group_name + + returned_result.append(result) + + return Response(returned_result) + + +class SharedRepo(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def put(self, request, repo_id, format=None): + """ Update permission of a shared repo. + + Permission checking: + 1. Only repo owner can update. + """ + + # argument check + permission = request.data.get('permission', None) + if permission not in ['r', 'rw']: + error_msg = 'permission invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + share_type = request.data.get('share_type', None) + if not share_type: + error_msg = 'share_type invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if share_type not in ('personal', 'group', 'public'): + error_msg = "share_type can only be 'personal' or 'group' or 'public'." + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # recourse check + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + username = request.user.username + if is_org_context(request): + repo_owner = seafile_api.get_org_repo_owner(repo_id) + else: + repo_owner = seafile_api.get_repo_owner(repo_id) + + if username != repo_owner: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # update share permission + if share_type == 'personal': + shared_to = request.data.get('user', None) + if not shared_to or not is_valid_username(shared_to): + error_msg = 'user invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + if is_org_context(request): + org_id = request.user.org.org_id + seaserv.seafserv_threaded_rpc.org_set_share_permission( + org_id, repo_id, username, shared_to, permission) + else: + seafile_api.set_share_permission(repo_id, + username, shared_to, permission) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + send_perm_audit_msg('modify-repo-perm', username, + shared_to, repo_id, '/', permission) + + if share_type == 'group': + group_id = request.data.get('group_id', None) + if not group_id: + error_msg = 'group_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + group_id = int(group_id) + except ValueError: + error_msg = 'group_id must be integer.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + group = ccnet_api.get_group(group_id) + if not group: + error_msg = 'Group %s not found.' % group_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + try: + if is_org_context(request): + org_id = request.user.org.org_id + seaserv.seafserv_threaded_rpc.set_org_group_repo_permission( + org_id, group_id, repo_id, permission) + else: + seafile_api.set_group_repo_permission( + group_id, repo_id, permission) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + send_perm_audit_msg('modify-repo-perm', username, + group_id, repo_id, '/', permission) + + if share_type == 'public': + try: + if is_org_context(request): + org_id = request.user.org.org_id + seaserv.seafserv_threaded_rpc.set_org_inner_pub_repo( + org_id, repo_id, permission) + else: + seafile_api.add_inner_pub_repo(repo_id, permission) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + send_perm_audit_msg('modify-repo-perm', username, + 'all', repo_id, '/', permission) + + return Response({'success': True}) + + def delete(self, request, repo_id, format=None): + """ Unshare a repo. + + Permission checking: + 1. Only repo owner can unshare a library. + """ + + # argument check + share_type = request.GET.get('share_type', None) + if not share_type: + error_msg = 'share_type invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if share_type not in ('personal', 'group', 'public'): + error_msg = "share_type can only be 'personal' or 'group' or 'public'." + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # resource check + repo = seafile_api.get_repo(repo_id) + if not repo: + return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id) + + # permission check + username = request.user.username + if is_org_context(request): + repo_owner = seafile_api.get_org_repo_owner(repo_id) + else: + repo_owner = seafile_api.get_repo_owner(repo_id) + + if username != repo_owner: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # delete share + org_id = None + if is_org_context(request): + org_id = request.user.org.org_id + + if share_type == 'personal': + user = request.GET.get('user', None) + if not user or not is_valid_username(user): + error_msg = 'user invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # if user not found, permission will be None + permission = seafile_api.check_permission_by_path( + repo_id, '/', user) + + try: + if org_id: + seafile_api.org_remove_share(org_id, repo_id, + username, user) + else: + seafile_api.remove_share(repo_id, username, user) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + send_perm_audit_msg('delete-repo-perm', username, user, + repo_id, '/', permission) + + if share_type == 'group': + group_id = request.GET.get('group_id', None) + if not group_id: + error_msg = 'group_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + group_id = int(group_id) + except ValueError: + error_msg = 'group_id must be integer.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # hacky way to get group repo permission + permission = '' + if org_id: + for e in seafile_api.list_org_repo_shared_group( + org_id, username, repo_id): + if e.group_id == group_id: + permission = e.perm + break + else: + for e in seafile_api.list_repo_shared_group_by_user(username, repo_id): + if e.group_id == group_id: + permission = e.perm + break + + try: + if org_id: + seaserv.del_org_group_repo(repo_id, org_id, group_id) + else: + seafile_api.unset_group_repo(repo_id, group_id, username) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + send_perm_audit_msg('delete-repo-perm', username, group_id, + repo_id, '/', permission) + + if share_type == 'public': + pub_repos = [] + if org_id: + pub_repos = seaserv.list_org_inner_pub_repos(org_id, username) + + if not request.cloud_mode: + pub_repos = seaserv.list_inner_pub_repos(username) + + try: + if org_id: + seaserv.seafserv_threaded_rpc.unset_org_inner_pub_repo(org_id, repo_id) + else: + seafile_api.remove_inner_pub_repo(repo_id) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + permission = '' + for repo in pub_repos: + if repo.repo_id == repo_id: + permission = repo.permission + break + + if permission: + send_perm_audit_msg('delete-repo-perm', username, 'all', repo_id, '/', permission) + + return Response({'success': True}) diff --git a/seahub/api2/endpoints/upload_links.py b/seahub/api2/endpoints/upload_links.py index 241b8ccc49..a2fb9e5a7d 100644 --- a/seahub/api2/endpoints/upload_links.py +++ b/seahub/api2/endpoints/upload_links.py @@ -1,3 +1,4 @@ +import os import logging from constance import config @@ -19,6 +20,7 @@ from seahub.api2.throttling import UserRateThrottle from seahub.share.models import UploadLinkShare from seahub.utils import gen_shared_upload_link from seahub.views import check_folder_permission +from seahub.utils.timeutils import datetime_to_isoformat_timestr logger = logging.getLogger(__name__) @@ -36,9 +38,30 @@ class UploadLinks(APIView): data = {} token = uls.token - data['repo_id'] = uls.repo_id - data['path'] = uls.path - data['ctime'] = uls.ctime + repo_id = uls.repo_id + try: + repo = seafile_api.get_repo(repo_id) + except Exception as e: + logger.error(e) + repo = None + + path = uls.path + if path: + obj_name = '/' if path == '/' else os.path.basename(path.rstrip('/')) + else: + obj_name = '' + + if uls.ctime: + ctime = datetime_to_isoformat_timestr(uls.ctime) + else: + ctime = '' + + data['repo_id'] = repo_id + data['repo_name'] = repo.repo_name if repo else '' + data['path'] = path + data['obj_name'] = obj_name + data['view_cnt'] = uls.view_cnt + data['ctime'] = ctime data['link'] = gen_shared_upload_link(token) data['token'] = token data['username'] = uls.username @@ -46,13 +69,20 @@ class UploadLinks(APIView): return data def get(self, request): - """ get upload link. + """ Get all upload links of a user. + + Permission checking: + 1. default(NOT guest) user; """ if not self._can_generate_shared_link(request): error_msg = 'Permission denied.' return api_error(status.HTTP_403_FORBIDDEN, error_msg) + # get all upload links + username = request.user.username + upload_link_shares = UploadLinkShare.objects.filter(username=username) + repo_id = request.GET.get('repo_id', None) if repo_id: repo = seafile_api.get_repo(repo_id) @@ -60,41 +90,27 @@ class UploadLinks(APIView): error_msg = 'Library %s not found.' % repo_id return api_error(status.HTTP_404_NOT_FOUND, error_msg) - # repo level permission check - if not check_folder_permission(request, repo_id, '/'): - error_msg = 'Permission denied.' - return api_error(status.HTTP_403_FORBIDDEN, error_msg) + # filter share links by repo + upload_link_shares = filter(lambda ufs: ufs.repo_id==repo_id, upload_link_shares) - path = request.GET.get('path', None) - if path: - try: - dir_id = seafile_api.get_dir_id_by_path(repo_id, path) - except SearpcError as e: - logger.error(e) - error_msg = 'Internal Server Error' - return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + path = request.GET.get('path', None) + if path: + try: + dir_id = seafile_api.get_dir_id_by_path(repo_id, path) + except SearpcError as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) - if not dir_id: - error_msg = 'folder %s not found.' % path - return api_error(status.HTTP_404_NOT_FOUND, error_msg) + if not dir_id: + error_msg = 'folder %s not found.' % path + return api_error(status.HTTP_404_NOT_FOUND, error_msg) - # folder permission check - if not check_folder_permission(request, repo_id, path): - error_msg = 'Permission denied.' - return api_error(status.HTTP_403_FORBIDDEN, error_msg) + if path[-1] != '/': + path = path + '/' - username = request.user.username - upload_link_shares = UploadLinkShare.objects.filter(username=username) - - # filter result by args - if repo_id: - upload_link_shares = filter(lambda ufs: ufs.repo_id == repo_id, upload_link_shares) - - if path: - if path[-1] != '/': - path = path + '/' - - upload_link_shares = filter(lambda ufs: ufs.path == path, upload_link_shares) + # filter share links by path + upload_link_shares = filter(lambda ufs: ufs.path==path, upload_link_shares) result = [] for uls in upload_link_shares: @@ -102,33 +118,42 @@ class UploadLinks(APIView): result.append(link_info) if len(result) == 1: - result = result[0] + result = result + else: + result.sort(lambda x, y: cmp(x['obj_name'], y['obj_name'])) return Response(result) def post(self, request): - """ create upload link. + """ Create upload link. + + Permission checking: + 1. default(NOT guest) user; + 2. user with 'rw' permission; """ - if not self._can_generate_shared_link(request): - error_msg = 'Permission denied.' - return api_error(status.HTTP_403_FORBIDDEN, error_msg) - + # argument check repo_id = request.data.get('repo_id', None) if not repo_id: error_msg = 'repo_id invalid.' return api_error(status.HTTP_400_BAD_REQUEST, error_msg) - repo = seafile_api.get_repo(repo_id) - if not repo: - error_msg = 'Library %s not found.' % repo_id - return api_error(status.HTTP_404_NOT_FOUND, error_msg) - path = request.data.get('path', None) if not path: error_msg = 'path invalid.' return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + password = request.data.get('password', None) + if password and len(password) < config.SHARE_LINK_PASSWORD_MIN_LENGTH: + error_msg = _('Password is too short') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # resource check + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + try: dir_id = seafile_api.get_dir_id_by_path(repo_id, path) except SearpcError as e: @@ -140,13 +165,12 @@ class UploadLinks(APIView): error_msg = 'folder %s not found.' % path return api_error(status.HTTP_404_NOT_FOUND, error_msg) - password = request.data.get('password', None) - if password and len(password) < config.SHARE_LINK_PASSWORD_MIN_LENGTH: - error_msg = _('Password is too short') - return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + # permission check + if not self._can_generate_shared_link(request): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) - user_perm = check_folder_permission(request, repo_id, '/') - if user_perm != 'rw': + if check_folder_permission(request, repo_id, path) != 'rw': error_msg = 'Permission denied.' return api_error(status.HTTP_403_FORBIDDEN, error_msg) @@ -170,20 +194,10 @@ class UploadLink(APIView): return request.user.permissions.can_generate_shared_link() def get(self, request, token): - """ get upload link info. - """ + """ Get upload link info. - try: - uls = UploadLinkShare.objects.get(token=token) - except UploadLinkShare.DoesNotExist: - error_msg = 'token %s not found.' % token - return api_error(status.HTTP_404_NOT_FOUND, error_msg) - - link_info = self._get_upload_link_info(uls) - return Response(link_info) - - def delete(self, request, token): - """ delete upload link. + Permission checking: + 1. default(NOT guest) user; """ if not self._can_generate_shared_link(request): @@ -196,6 +210,26 @@ class UploadLink(APIView): error_msg = 'token %s not found.' % token return api_error(status.HTTP_404_NOT_FOUND, error_msg) + link_info = self._get_upload_link_info(uls) + return Response(link_info) + + def delete(self, request, token): + """ Delete upload link. + + Permission checking: + 1. default(NOT guest) user; + 2. link owner; + """ + + if not self._can_generate_shared_link(request): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + uls = UploadLinkShare.objects.get(token=token) + except UploadLinkShare.DoesNotExist: + return Response({'success': True}) + username = request.user.username if not uls.is_owner(username): error_msg = 'Permission denied.' @@ -203,8 +237,9 @@ class UploadLink(APIView): try: uls.delete() - return Response({'success': True}) except Exception as e: logger.error(e) error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + return Response({'success': True}) diff --git a/seahub/api2/views.py b/seahub/api2/views.py index ae9c563414..c1f51c1395 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -2959,7 +2959,6 @@ class SharedLinksView(APIView): permission_classes = (IsAuthenticated,) throttle_classes = (UserRateThrottle, ) - # from seahub.share.view::list_shared_links def get(self, request, format=None): username = request.user.username diff --git a/seahub/share/templates/share/links.html b/seahub/share/templates/share/links.html deleted file mode 100644 index 69a88308aa..0000000000 --- a/seahub/share/templates/share/links.html +++ /dev/null @@ -1,259 +0,0 @@ -{% extends 'home_base.html' %} -{% load seahub_tags i18n %} -{% load url from future %} - -{% block sub_title %}{% trans "Links - Share" %} - {% endblock %} - -{% block cur_share_links %}tab-cur{% endblock %} - -{% block right_panel %} -
- - - - -
- -{% endblock %} - -{% block extra_script %}{{block.super}} -{% if fileshares %} - -{% endif %} - -{% endblock %} diff --git a/seahub/share/templates/share/list_priv_shared_folders.html b/seahub/share/templates/share/list_priv_shared_folders.html deleted file mode 100644 index 648361e18c..0000000000 --- a/seahub/share/templates/share/list_priv_shared_folders.html +++ /dev/null @@ -1,117 +0,0 @@ -{% extends 'home_base.html' %} -{% load seahub_tags i18n %} -{% load url from future %} - -{% block sub_title %}{% trans "Folders - Share" %} - {% endblock %} - -{% block cur_share_folders %}tab-cur{% endblock %} - -{% block right_panel %} -

{% trans "Folders" %}

-{% if shared_folders %} - - - - - - - - - {% for repo in shared_folders %} - - - - - - - - {% endfor %} -
{% trans "Name"%}{% trans "Share To"%}{% trans "Permission"%}
{% trans {{ repo.props.repo_name }}{{ repo.props.user | email2nickname }} - - - - {% if repo.props.share_type == 'group' %} - - {% endif %} - {% if repo.props.share_type == 'personal' %} - - {% endif %} - {% if repo.props.share_type == 'public' %} - - {% endif %} -
-{% else %} -
-

{% trans "You have not shared any folder" %}

-

{% trans "You can share a single folder with a registered user if you don't want to share a whole library." %}

-
-{% endif %} -{% endblock %} - -{% block extra_script %}{{block.super}} - -{% endblock %} diff --git a/seahub/share/templates/share/repos.html b/seahub/share/templates/share/repos.html deleted file mode 100644 index a307ac4ea0..0000000000 --- a/seahub/share/templates/share/repos.html +++ /dev/null @@ -1,126 +0,0 @@ -{% extends 'home_base.html' %} -{% load seahub_tags i18n %} -{% load url from future %} - -{% block sub_title %}{% trans "Libraries - Share" %} - {% endblock %} - -{% block cur_share_libs %}tab-cur{% endblock %} - -{% block right_panel %} -

{% trans "Libraries" %}

-{% if out_repos %} - - - - - - - - - {% for repo in out_repos %} - - {% if repo.encrypted %} - - {% else %} - - {% endif %} - - - - - - {% endfor %} -
{% trans "Name"%} {% trans "Share To"%}{% trans "Permission"%}
{% trans {% trans {{ repo.props.repo_name }}{{ repo.props.user | email2nickname }} - - - - {% if repo.props.share_type == 'group' %} - - {% endif %} - {% if repo.props.share_type == 'personal' %} - - {% endif %} - {% if repo.props.share_type == 'public' %} - - {% endif %} -
-{% else %} -
-

{% trans "You have not shared any library" %}

-

{% trans "You can share libraries to your friends and colleagues by clicking the share icon of your own libraries in your home page or creating a new library in groups you are in." %}

-
-{% endif %} -{% endblock %} - -{% block extra_script %}{{block.super}} - -{% endblock %} diff --git a/seahub/share/urls.py b/seahub/share/urls.py index 89c33bb7c7..d808f13a90 100644 --- a/seahub/share/urls.py +++ b/seahub/share/urls.py @@ -3,19 +3,9 @@ from django.conf.urls import patterns, url from views import * urlpatterns = patterns('', - url(r'^$', list_shared_repos, name='share_admin'), - url(r'^links/$', list_shared_links, name='list_shared_links'), - url(r'^folders/$', list_priv_shared_folders, name='list_priv_shared_folders'), - url(r'^add/$', share_repo, name='share_repo'), - url(r'^remove/$', repo_remove_share, name='repo_remove_share'), - url(r'^ajax/link/remove/$', ajax_remove_shared_link, name='ajax_remove_shared_link'), url(r'^link/send/$', send_shared_link, name='send_shared_link'), url(r'^link/save/$', save_shared_link, name='save_shared_link'), - url(r'^ajax/upload_link/remove/$', ajax_remove_shared_upload_link, name='ajax_remove_shared_upload_link'), url(r'^upload_link/send/$', send_shared_upload_link, name='send_shared_upload_link'), - url(r'^permission_admin/$', share_permission_admin, name='share_permission_admin'), - url(r'^ajax/get-download-link/$', ajax_get_download_link, name='ajax_get_download_link'), - url(r'^ajax/get-upload-link/$', ajax_get_upload_link, name='ajax_get_upload_link'), url(r'^ajax/private-share-dir/$', ajax_private_share_dir, name='ajax_private_share_dir'), url(r'^ajax/get-link-audit-code/$', ajax_get_link_audit_code, name='ajax_get_link_audit_code'), ) diff --git a/seahub/share/views.py b/seahub/share/views.py index 0bc290ce26..218d782884 100644 --- a/seahub/share/views.py +++ b/seahub/share/views.py @@ -6,35 +6,29 @@ from dateutil.relativedelta import relativedelta from constance import config from django.core.cache import cache -from django.core.urlresolvers import reverse from django.http import HttpResponse, HttpResponseRedirect, Http404, \ HttpResponseBadRequest -from django.shortcuts import render_to_response -from django.template import RequestContext from django.utils.translation import ugettext as _ from django.contrib import messages from django.utils import timezone from django.utils.html import escape -# from django.contrib.sites.models import RequestSite import seaserv from seaserv import seafile_api -from seaserv import ccnet_threaded_rpc, is_org_group, \ - get_org_id_by_group, del_org_group_repo, unset_inner_pub_repo +from seaserv import ccnet_threaded_rpc from pysearpc import SearpcError -from seahub.share.forms import RepoShareForm, FileLinkShareForm, \ +from seahub.share.forms import FileLinkShareForm, \ UploadLinkShareForm from seahub.share.models import FileShare, UploadLinkShare, OrgFileShare from seahub.share.signals import share_repo_to_user_successful from seahub.auth.decorators import login_required, login_required_ajax -from seahub.base.decorators import user_mods_check, require_POST +from seahub.base.decorators import require_POST from seahub.contacts.signals import mail_sended from seahub.views import is_registered_user, check_folder_permission -from seahub.utils import render_permission_error, string2list, render_error, \ - gen_shared_link, gen_shared_upload_link, gen_dir_share_link, \ - gen_file_share_link, IS_EMAIL_CONFIGURED, check_filename_with_rename, \ +from seahub.utils import string2list, gen_shared_link, \ + gen_shared_upload_link, IS_EMAIL_CONFIGURED, check_filename_with_rename, \ is_valid_username, is_valid_email, send_html_email, is_org_context, \ - send_perm_audit_msg, get_origin_repo_info, gen_token, normalize_cache_key + gen_token, normalize_cache_key from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY from seahub.settings import SITE_ROOT, REPLACE_FROM_EMAIL, ADD_REPLY_TO_HEADER from seahub.profile.models import Profile @@ -47,14 +41,6 @@ def is_org_repo_owner(username, repo_id): owner = seaserv.seafserv_threaded_rpc.get_org_repo_owner(repo_id) return True if owner == username else False -def get_org_group_repos_by_owner(org_id, username): - return seaserv.seafserv_threaded_rpc.get_org_group_repos_by_owner(org_id, - username) - -def list_org_inner_pub_repos_by_owner(org_id, username): - return seaserv.seafserv_threaded_rpc.list_org_inner_pub_repos_by_owner( - org_id, username) - def org_share_repo(org_id, repo_id, from_user, to_user, permission): return seaserv.seafserv_threaded_rpc.org_add_share(org_id, repo_id, from_user, to_user, @@ -65,27 +51,6 @@ def org_remove_share(org_id, repo_id, from_user, to_user): from_user, to_user) ########## functions - -def share_to_public(request, repo, permission): - """Share repo to public with given permission. - """ - try: - if is_org_context(request): - org_id = request.user.org.org_id - seaserv.seafserv_threaded_rpc.set_org_inner_pub_repo( - org_id, repo.id, permission) - elif request.cloud_mode: - return # no share to public in cloud mode - else: - seafile_api.add_inner_pub_repo(repo.id, permission) - except Exception, e: - logger.error(e) - messages.error(request, _(u'Failed to share to all members, please try again later.')) - else: - msg = _(u'Shared to all members successfully, go check it at Shares.') % \ - (reverse('share_admin')) - messages.success(request, msg, extra_tags='safe') - def share_to_group(request, repo, group, permission): """Share repo to group with given permission. """ @@ -149,495 +114,7 @@ def share_to_user(request, repo, to_user, permission): to_user=to_user, repo=repo) return True - -########## views -@login_required -@require_POST -def share_repo(request): - """ - Handle POST method to share a repo to public/groups/users based on form - data. Return to ``myhome`` page and notify user whether success or failure. - """ - next = request.META.get('HTTP_REFERER', None) - if not next: - next = SITE_ROOT - - form = RepoShareForm(request.POST) - if not form.is_valid(): - # TODO: may display error msg on form - raise Http404 - - email_or_group = form.cleaned_data['email_or_group'] - repo_id = form.cleaned_data['repo_id'] - permission = form.cleaned_data['permission'] - - repo = seafile_api.get_repo(repo_id) - if not repo: - raise Http404 - - # Test whether user is the repo owner. - username = request.user.username - if not seafile_api.is_repo_owner(username, repo_id) and \ - not is_org_repo_owner(username, repo_id): - msg = _(u'Only the owner of the library has permission to share it.') - messages.error(request, msg) - return HttpResponseRedirect(next) - - # Parsing input values. - share_to_all, share_to_groups, share_to_users = False, [], [] - user_groups = request.user.joined_groups - share_to_list = string2list(email_or_group) - for share_to in share_to_list: - if share_to == 'all': - share_to_all = True - elif share_to.find('@') == -1: - for user_group in user_groups: - if user_group.group_name == share_to: - share_to_groups.append(user_group) - else: - share_to = share_to.lower() - if is_valid_username(share_to): - share_to_users.append(share_to) - - origin_repo_id, origin_path = get_origin_repo_info(repo.id) - if origin_repo_id is not None: - perm_repo_id = origin_repo_id - perm_path = origin_path - else: - perm_repo_id = repo.id - perm_path = '/' - - if share_to_all: - share_to_public(request, repo, permission) - send_perm_audit_msg('add-repo-perm', username, 'all', \ - perm_repo_id, perm_path, permission) - - for group in share_to_groups: - if share_to_group(request, repo, group, permission): - send_perm_audit_msg('add-repo-perm', username, group.id, \ - perm_repo_id, perm_path, permission) - - for email in share_to_users: - # Add email to contacts. - mail_sended.send(sender=None, user=request.user.username, email=email) - if share_to_user(request, repo, email, permission): - send_perm_audit_msg('add-repo-perm', username, email, \ - perm_repo_id, perm_path, permission) - - return HttpResponseRedirect(next) - -@login_required -@require_POST -def repo_remove_share(request): - """ - If repo is shared from one person to another person, only these two person - can remove share. - If repo is shared from one person to a group, then only the one share the - repo and group staff can remove share. - """ - repo_id = request.GET.get('repo_id', '') - group_id = request.GET.get('gid', '') - from_email = request.GET.get('from', '') - perm = request.GET.get('permission', None) - if not is_valid_username(from_email) or perm is None: - return render_error(request, _(u'Argument is not valid')) - username = request.user.username - - repo = seafile_api.get_repo(repo_id) - if not repo: - return render_error(request, _(u'Library does not exist')) - - origin_repo_id, origin_path = get_origin_repo_info(repo.id) - if origin_repo_id is not None: - perm_repo_id = origin_repo_id - perm_path = origin_path - else: - perm_repo_id = repo.id - perm_path = '/' - - # if request params don't have 'gid', then remove repos that share to - # to other person; else, remove repos that share to groups - if not group_id: - to_email = request.GET.get('to', '') - if not is_valid_username(to_email): - return render_error(request, _(u'Argument is not valid')) - - if username != from_email and username != to_email: - return render_permission_error(request, _(u'Failed to remove share')) - - if is_org_context(request): - org_id = request.user.org.org_id - org_remove_share(org_id, repo_id, from_email, to_email) - else: - seaserv.remove_share(repo_id, from_email, to_email) - send_perm_audit_msg('delete-repo-perm', from_email, to_email, \ - perm_repo_id, perm_path, perm) - else: - try: - group_id = int(group_id) - except: - return render_error(request, _(u'group id is not valid')) - - group = seaserv.get_group(group_id) - if not group: - return render_error(request, _(u"Failed to unshare: the group doesn't exist.")) - - if not seaserv.check_group_staff(group_id, username) \ - and username != from_email: - return render_permission_error(request, _(u'Failed to remove share')) - - if is_org_group(group_id): - org_id = get_org_id_by_group(group_id) - del_org_group_repo(repo_id, org_id, group_id) - else: - seafile_api.unset_group_repo(repo_id, group_id, from_email) - send_perm_audit_msg('delete-repo-perm', from_email, group_id, \ - perm_repo_id, perm_path, perm) - - messages.success(request, _('Successfully removed share')) - - next = request.META.get('HTTP_REFERER', SITE_ROOT) - return HttpResponseRedirect(next) - -def get_share_out_repo_list(request): - """List repos that @user share to other users. - - Returns: - A list of repos. - """ - username = request.user.username - if is_org_context(request): - org_id = request.user.org.org_id - return seafile_api.get_org_share_out_repo_list(org_id, username, - -1, -1) - else: - return seafile_api.get_share_out_repo_list(username, -1, -1) - -def get_group_repos_by_owner(request): - """List repos that @user share to groups. - - Returns: - A list of repos. - """ - username = request.user.username - if is_org_context(request): - org_id = request.user.org.org_id - return get_org_group_repos_by_owner(org_id, username) - else: - return seaserv.get_group_repos_by_owner(username) - -def list_inner_pub_repos_by_owner(request): - """List repos that @user share to organizatoin. - - Returns: - A list of repos, or empty list if in cloud_mode. - """ - username = request.user.username - if is_org_context(request): - org_id = request.user.org.org_id - return list_org_inner_pub_repos_by_owner(org_id, username) - elif request.cloud_mode: - return [] - else: - return seaserv.list_inner_pub_repos_by_owner(username) - -def list_share_out_repos(request): - shared_repos = [] - - # repos shared from this user - shared_repos += get_share_out_repo_list(request) - - # repos shared to groups - group_repos = get_group_repos_by_owner(request) - for repo in group_repos: - group = ccnet_threaded_rpc.get_group(int(repo.group_id)) - if not group: - repo.props.user = '' - continue - repo.props.user = group.props.group_name - repo.props.user_info = repo.group_id - shared_repos += group_repos - - # inner pub repos - pub_repos = list_inner_pub_repos_by_owner(request) - for repo in pub_repos: - repo.props.user = _(u'all members') - repo.props.user_info = 'all' - shared_repos += pub_repos - - return shared_repos - -@login_required -@user_mods_check -def list_shared_repos(request): - """ List user repos shared to users/groups/public. - """ - share_out_repos = list_share_out_repos(request) - - out_repos = [] - for repo in share_out_repos: - if repo.is_virtual: # skip virtual repos - continue - - if repo.props.permission == 'rw': - repo.share_permission = _(u'Read-Write') - elif repo.props.permission == 'r': - repo.share_permission = _(u'Read-Only') - else: - repo.share_permission = '' - - if repo.props.share_type == 'personal': - repo.props.user_info = repo.props.user - out_repos.append(repo) - - out_repos.sort(lambda x, y: cmp(x.repo_name, y.repo_name)) - - return render_to_response('share/repos.html', { - "out_repos": out_repos, - }, context_instance=RequestContext(request)) - -@login_required -@user_mods_check -def list_shared_links(request): - """List shared links, and remove invalid links(file/dir is deleted or moved). - """ - username = request.user.username - - # download links - fileshares = FileShare.objects.filter(username=username) - fs_files, fs_dirs = [], [] - for fs in fileshares: - r = seafile_api.get_repo(fs.repo_id) - if not r: - fs.delete() - continue - - if fs.is_file_share_link(): - if seafile_api.get_file_id_by_path(r.id, fs.path) is None: - fs.delete() - continue - fs.filename = os.path.basename(fs.path) - fs.shared_link = gen_file_share_link(fs.token) - else: - if seafile_api.get_dir_id_by_path(r.id, fs.path) is None: - fs.delete() - continue - if fs.path != '/': - fs.filename = os.path.basename(fs.path.rstrip('/')) - else: - fs.filename = fs.path - fs.shared_link = gen_dir_share_link(fs.token) - fs.repo = r - - if fs.expire_date is not None and timezone.now() > fs.expire_date: - fs.is_expired = True - - fs_files.append(fs) if fs.is_file_share_link() else fs_dirs.append(fs) - fs_files.sort(lambda x, y: cmp(x.filename, y.filename)) - fs_dirs.sort(lambda x, y: cmp(x.filename, y.filename)) - - # upload links - uploadlinks = UploadLinkShare.objects.filter(username=username) - p_uploadlinks = [] - for link in uploadlinks: - r = seafile_api.get_repo(link.repo_id) - if not r: - link.delete() - continue - if seafile_api.get_dir_id_by_path(r.id, link.path) is None: - link.delete() - continue - if link.path != '/': - link.dir_name = os.path.basename(link.path.rstrip('/')) - else: - link.dir_name = link.path - link.shared_link = gen_shared_upload_link(link.token) - link.repo = r - p_uploadlinks.append(link) - p_uploadlinks.sort(lambda x, y: cmp(x.dir_name, y.dir_name)) - - return render_to_response('share/links.html', { - "fileshares": fs_dirs + fs_files, - "uploadlinks": p_uploadlinks, - }, context_instance=RequestContext(request)) - -@login_required -@user_mods_check -def list_priv_shared_folders(request): - """List private shared folders. - - Arguments: - - `request`: - """ - share_out_repos = list_share_out_repos(request) - - shared_folders = [] - for repo in share_out_repos: - if not repo.is_virtual: # skip non-virtual repos - continue - - if repo.props.permission == 'rw': - repo.share_permission = _(u'Read-Write') - elif repo.props.permission == 'r': - repo.share_permission = _(u'Read-Only') - else: - repo.share_permission = '' - - if repo.props.share_type == 'personal': - repo.props.user_info = repo.props.user - shared_folders.append(repo) - - shared_folders.sort(lambda x, y: cmp(x.repo_id, y.repo_id)) - - return render_to_response('share/list_priv_shared_folders.html', { - 'shared_folders': shared_folders, - }, context_instance=RequestContext(request)) - -@login_required_ajax -def share_permission_admin(request): - """Change repo share permission in ShareAdmin. - """ - share_type = request.GET.get('share_type', '') - content_type = 'application/json; charset=utf-8' - - form = RepoShareForm(request.POST) - form.is_valid() - - email_or_group = form.cleaned_data['email_or_group'] - repo_id = form.cleaned_data['repo_id'] - permission = form.cleaned_data['permission'] - from_email = request.user.username - - repo = seafile_api.get_repo(repo_id) - if not repo: - return render_error(request, _(u'Library does not exist')) - - origin_repo_id, origin_path = get_origin_repo_info(repo.id) - if origin_repo_id is not None: - perm_repo_id = origin_repo_id - perm_path = origin_path - else: - perm_repo_id = repo.id - perm_path = '/' - - - if share_type == 'personal': - if not is_valid_username(email_or_group): - return HttpResponse(json.dumps({'success': False}), status=400, - content_type=content_type) - - try: - if is_org_context(request): - org_id = request.user.org.org_id - seaserv.seafserv_threaded_rpc.org_set_share_permission( - org_id, repo_id, from_email, email_or_group, permission) - else: - seafile_api.set_share_permission(repo_id, from_email, - email_or_group, permission) - send_perm_audit_msg('modify-repo-perm', from_email, \ - email_or_group, perm_repo_id, perm_path, permission) - - except SearpcError: - return HttpResponse(json.dumps({'success': False}), status=500, - content_type=content_type) - return HttpResponse(json.dumps({'success': True}), - content_type=content_type) - - elif share_type == 'group': - try: - if is_org_context(request): - org_id = request.user.org.org_id - seaserv.seafserv_threaded_rpc.set_org_group_repo_permission( - org_id, int(email_or_group), repo_id, permission) - else: - group_id = int(email_or_group) - seafile_api.set_group_repo_permission(group_id, - repo_id, - permission) - send_perm_audit_msg('modify-repo-perm', from_email, \ - group_id, perm_repo_id, perm_path, permission) - except SearpcError: - return HttpResponse(json.dumps({'success': False}), status=500, - content_type=content_type) - return HttpResponse(json.dumps({'success': True}), - content_type=content_type) - - elif share_type == 'public': - try: - if is_org_context(request): - org_id = request.user.org.org_id - seaserv.seafserv_threaded_rpc.set_org_inner_pub_repo( - org_id, repo_id, permission) - else: - seafile_api.add_inner_pub_repo(repo_id, permission) - send_perm_audit_msg('modify-repo-perm', from_email, 'all', \ - perm_repo_id, perm_path, permission) - except SearpcError: - return HttpResponse(json.dumps({'success': False}), status=500, - content_type=content_type) - return HttpResponse(json.dumps({'success': True}), - content_type=content_type) - - else: - return HttpResponse(json.dumps({'success': False}), status=400, - content_type=content_type) - ########## share link -@login_required_ajax -@require_POST -def ajax_remove_shared_link(request): - username = request.user.username - content_type = 'application/json; charset=utf-8' - result = {} - - token = request.POST.get('t') - if not token: - result = {'error': _(u"Argument missing")} - return HttpResponse(json.dumps(result), status=400, content_type=content_type) - - try: - link = FileShare.objects.get(token=token) - except FileShare.DoesNotExist: - result = {'error': _(u"The link doesn't exist")} - return HttpResponse(json.dumps(result), status=400, content_type=content_type) - - if not link.is_owner(username): - result = {'error': _("Permission denied")} - return HttpResponse(json.dumps(result), status=403, - content_type=content_type) - - link.delete() - result = {'success': True} - return HttpResponse(json.dumps(result), content_type=content_type) - - -@login_required_ajax -@require_POST -def ajax_remove_shared_upload_link(request): - username = request.user.username - content_type = 'application/json; charset=utf-8' - result = {} - - token = request.POST.get('t') - if not token: - result = {'error': _(u"Argument missing")} - return HttpResponse(json.dumps(result), status=400, content_type=content_type) - - try: - upload_link = UploadLinkShare.objects.get(token=token) - except UploadLinkShare.DoesNotExist: - result = {'error': _(u"The link doesn't exist")} - return HttpResponse(json.dumps(result), status=400, content_type=content_type) - - if not upload_link.is_owner(username): - result = {'error': _("Permission denied")} - return HttpResponse(json.dumps(result), status=403, - content_type=content_type) - upload_link.delete() - result = {'success': True} - return HttpResponse(json.dumps(result), content_type=content_type) - - @login_required_ajax def send_shared_link(request): """ @@ -835,282 +312,6 @@ def send_shared_upload_link(request): else: return HttpResponseBadRequest(json.dumps(form.errors), content_type=content_type) -@login_required_ajax -def ajax_get_upload_link(request): - content_type = 'application/json; charset=utf-8' - - if request.method == 'GET': - repo_id = request.GET.get('repo_id', None) - path = request.GET.get('p', None) - - # augument check - if not repo_id: - data = json.dumps({'error': 'repo_id invalid.'}) - return HttpResponse(data, status=400, content_type=content_type) - - if not path: - data = json.dumps({'error': 'p invalid.'}) - return HttpResponse(data, status=400, content_type=content_type) - - # resource check - try: - repo = seafile_api.get_repo(repo_id) - except Exception as e: - logger.error(e) - data = json.dumps({'error': 'Internal Server Error'}) - return HttpResponse(data, status=500, content_type=content_type) - - if not repo: - data = json.dumps({'error': 'Library %s not found.' % repo_id}) - return HttpResponse(data, status=404, content_type=content_type) - - if not path.endswith('/'): - path = path + '/' - - if not seafile_api.get_dir_id_by_path(repo_id, path): - data = json.dumps({'error': 'Folder %s not found.' % path}) - return HttpResponse(data, status=404, content_type=content_type) - - # permission check - if not check_folder_permission(request, repo_id, path): - data = json.dumps({'error': 'Permission denied.'}) - return HttpResponse(data, status=403, content_type=content_type) - - # get upload link - username = request.user.username - l = UploadLinkShare.objects.filter(repo_id=repo_id).filter( - username=username).filter(path=path) - - data = {} - if len(l) > 0: - token = l[0].token - data['upload_link'] = gen_shared_upload_link(token) - data['token'] = token - - return HttpResponse(json.dumps(data), content_type=content_type) - - elif request.method == 'POST': - repo_id = request.POST.get('repo_id', None) - path = request.POST.get('p', None) - use_passwd = True if int(request.POST.get('use_passwd', '0')) == 1 else False - passwd = request.POST.get('passwd') if use_passwd else None - - # augument check - if not repo_id: - data = json.dumps({'error': 'repo_id invalid.'}) - return HttpResponse(data, status=400, content_type=content_type) - - if not path: - data = json.dumps({'error': 'p invalid.'}) - return HttpResponse(data, status=400, content_type=content_type) - - if passwd and len(passwd) < config.SHARE_LINK_PASSWORD_MIN_LENGTH: - data = json.dumps({'error': _('Password is too short')}) - return HttpResponse(data, status=400, content_type=content_type) - - # resource check - try: - repo = seafile_api.get_repo(repo_id) - except Exception as e: - logger.error(e) - data = json.dumps({'error': 'Internal Server Error'}) - return HttpResponse(data, status=500, content_type=content_type) - - if not repo: - data = json.dumps({'error': 'Library %s not found.' % repo_id}) - return HttpResponse(data, status=404, content_type=content_type) - - if not path.endswith('/'): - path = path + '/' - - if not seafile_api.get_dir_id_by_path(repo_id, path): - data = json.dumps({'error': 'Folder %s not found.' % path}) - return HttpResponse(data, status=404, content_type=content_type) - - # permission check - # normal permission check & default/guest user permission check - if check_folder_permission(request, repo_id, path) != 'rw' or \ - not request.user.permissions.can_generate_shared_link(): - data = json.dumps({'error': 'Permission denied.'}) - return HttpResponse(data, status=403, content_type=content_type) - - # generate upload link - l = UploadLinkShare.objects.filter(repo_id=repo_id).filter( - username=request.user.username).filter(path=path) - - if len(l) > 0: - # if already exist - upload_link = l[0] - token = upload_link.token - else: - # generate new - username = request.user.username - uls = UploadLinkShare.objects.create_upload_link_share( - username, repo_id, path, passwd) - token = uls.token - - shared_upload_link = gen_shared_upload_link(token) - data = json.dumps({'token': token, 'upload_link': shared_upload_link}) - - return HttpResponse(data, content_type=content_type) - -@login_required_ajax -def ajax_get_download_link(request): - """ - Handle ajax request to generate file or dir shared link. - """ - content_type = 'application/json; charset=utf-8' - - if request.method == 'GET': - repo_id = request.GET.get('repo_id', None) - share_type = request.GET.get('type', 'f') - path = request.GET.get('p', None) - - # augument check - if not repo_id: - data = json.dumps({'error': 'repo_id invalid.'}) - return HttpResponse(data, status=400, content_type=content_type) - - if not path: - data = json.dumps({'error': 'p invalid.'}) - return HttpResponse(data, status=400, content_type=content_type) - - if share_type not in ('f', 'd'): - data = json.dumps({'error': 'type invalid.'}) - return HttpResponse(data, status=400, content_type=content_type) - - # resource check - try: - repo = seafile_api.get_repo(repo_id) - except Exception as e: - logger.error(e) - data = json.dumps({'error': 'Internal Server Error'}) - return HttpResponse(data, status=500, content_type=content_type) - - if not repo: - data = json.dumps({'error': 'Library %s not found.' % repo_id}) - return HttpResponse(data, status=404, content_type=content_type) - - if share_type == 'f': - if not seafile_api.get_file_id_by_path(repo_id, path): - data = json.dumps({'error': 'File %s not found.' % path}) - return HttpResponse(data, status=404, content_type=content_type) - - if share_type == 'd': - if not path.endswith('/'): - path = path + '/' - - if not seafile_api.get_dir_id_by_path(repo_id, path): - data = json.dumps({'error': 'Folder %s not found.' % path}) - return HttpResponse(data, status=404, content_type=content_type) - - # permission check - if not check_folder_permission(request, repo_id, path): - data = json.dumps({'error': 'Permission denied.'}) - return HttpResponse(data, status=403, content_type=content_type) - - # get download link - username = request.user.username - l = FileShare.objects.filter(repo_id=repo_id).filter( - username=username).filter(path=path) - - data = {} - if len(l) > 0: - token = l[0].token - data['download_link'] = gen_shared_link(token, l[0].s_type) - data['token'] = token - data['is_expired'] = l[0].is_expired() - - return HttpResponse(json.dumps(data), content_type=content_type) - - elif request.method == 'POST': - repo_id = request.POST.get('repo_id', None) - path = request.POST.get('p', None) - share_type = request.POST.get('type', 'f') - use_passwd = True if int(request.POST.get('use_passwd', '0')) == 1 else False - passwd = request.POST.get('passwd') if use_passwd else None - - # augument check - if not repo_id: - data = json.dumps({'error': 'repo_id invalid.'}) - return HttpResponse(data, status=400, content_type=content_type) - - if not path: - data = json.dumps({'error': 'p invalid.'}) - return HttpResponse(data, status=400, content_type=content_type) - - if share_type not in ('f', 'd'): - data = json.dumps({'error': 'type invalid.'}) - return HttpResponse(data, status=400, content_type=content_type) - - if passwd and len(passwd) < config.SHARE_LINK_PASSWORD_MIN_LENGTH: - data = json.dumps({'error': _('Password is too short')}) - return HttpResponse(data, status=400, content_type=content_type) - - try: - expire_days = int(request.POST.get('expire_days', 0)) - except ValueError: - expire_days = 0 - - if expire_days <= 0: - expire_date = None - else: - expire_date = timezone.now() + relativedelta(days=expire_days) - - # resource check - try: - repo = seafile_api.get_repo(repo_id) - except Exception as e: - logger.error(e) - data = json.dumps({'error': 'Internal Server Error'}) - return HttpResponse(data, status=500, content_type=content_type) - - if not repo: - data = json.dumps({'error': 'Library %s not found.' % repo_id}) - return HttpResponse(data, status=404, content_type=content_type) - - if share_type == 'f': - if not seafile_api.get_file_id_by_path(repo_id, path): - data = json.dumps({'error': 'File %s not found.' % path}) - return HttpResponse(data, status=404, content_type=content_type) - - if share_type == 'd': - if not path.endswith('/'): - path = path + '/' - - if not seafile_api.get_dir_id_by_path(repo_id, path): - data = json.dumps({'error': 'Folder %s not found.' % path}) - return HttpResponse(data, status=404, content_type=content_type) - - # permission check - # normal permission check & default/guest user permission check - if check_folder_permission(request, repo_id, path) != 'rw' or \ - not request.user.permissions.can_generate_shared_link(): - data = json.dumps({'error': 'Permission denied.'}) - return HttpResponse(data, status=403, content_type=content_type) - - username = request.user.username - if share_type == 'f': - fs = FileShare.objects.get_file_link_by_path(username, repo_id, path) - if fs is None: - fs = FileShare.objects.create_file_link(username, repo_id, path, - passwd, expire_date) - if is_org_context(request): - org_id = request.user.org.org_id - OrgFileShare.objects.set_org_file_share(org_id, fs) - else: - fs = FileShare.objects.get_dir_link_by_path(username, repo_id, path) - if fs is None: - fs = FileShare.objects.create_dir_link(username, repo_id, path, - passwd, expire_date) - if is_org_context(request): - org_id = request.user.org.org_id - OrgFileShare.objects.set_org_file_share(org_id, fs) - - token = fs.token - shared_link = gen_shared_link(token, fs.s_type) - data = json.dumps({'token': token, 'download_link': shared_link}) - return HttpResponse(data, content_type=content_type) @login_required_ajax @require_POST diff --git a/seahub/templates/home_base.html b/seahub/templates/home_base.html index b2dd36080b..38e3b0f54f 100644 --- a/seahub/templates/home_base.html +++ b/seahub/templates/home_base.html @@ -50,10 +50,10 @@

{% trans "Share Admin" %}

{% endblock %} diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html index f7146b72e1..45a5b2dd33 100644 --- a/seahub/templates/js/templates.html +++ b/seahub/templates/js/templates.html @@ -692,15 +692,15 @@

{% trans "Share Admin" %}

@@ -1502,3 +1502,184 @@

<% } %> + + + + + + + + + + + + + + + + diff --git a/seahub/templates/snippets/shared_link_js.html b/seahub/templates/snippets/shared_link_js.html index c02b68bbfb..423d1d09b7 100644 --- a/seahub/templates/snippets/shared_link_js.html +++ b/seahub/templates/snippets/shared_link_js.html @@ -1,24 +1,6 @@ {% load i18n %} {% load url from future %} -/* -var share_list = []; -$(function () { - $.ajax({ - url:'{% url 'get_contacts' %}', - cache: false, - dataType: 'json', - success: function(data) { - var contact_list = data['contacts'], contact_email; - for (var i = 0, len = contact_list.length; i < len; i++) { - contact_email = contact_list[i].email; - share_list.push({value: contact_email, label: contact_email}); - } - } - }); -}); -*/ - function showSharePopup(op, name, aj_data, type, cur_path) { var path = cur_path + name; @@ -183,13 +165,13 @@ $('#gen-link-btn').click(function() { } $.ajax({ - url: '{% url 'ajax_get_download_link' %}', + url: '{% url 'api-v2.1-share-links' %}', type: 'POST', dataType: 'json', beforeSend: prepareCSRFToken, data: $.extend(post_data, $(this).data('aj_data')), success: function(data) { - var link = data['download_link']; + var link = data['link']; // hide gen-link button, and show link gen_link_btn.addClass('hide'); @@ -215,9 +197,8 @@ $('#rm-shared-link').click(function() { token = obj.attr('data-token'); $.ajax({ - url: '{% url 'ajax_remove_shared_link' %}', - type: 'POST', - data: {'t': token}, + url: '{{ SITE_ROOT }}api/v2.1/share-links/' + token + '/', + type: 'DELETE', dataType: 'json', cache: false, beforeSend: prepareCSRFToken, @@ -247,7 +228,7 @@ $('#link-passwd-switch').click(function () { $('#link-expire-switch').click(function () { var form = $('#link-options'), days_input = $('input[name="expire-days"]', form); - var link_expire = $('#link-expire'); + var link_expire = $('#link-expire'); if ($(this).prop('checked')) { link_expire.slideDown(100); diff --git a/seahub/templates/snippets/sort_lib_js.html b/seahub/templates/snippets/sort_lib_js.html deleted file mode 100644 index 2bc07e03f2..0000000000 --- a/seahub/templates/snippets/sort_lib_js.html +++ /dev/null @@ -1,11 +0,0 @@ -$(function() { - $.ajax({ - url: '{{MEDIA_URL}}js/sort_lib.js', - dataType: 'script', - cache: true, - success: function() { - $("span[id$='repo-list-name-down'], span[id$='repo-list-name-up']").on('click',sort_lib_by_name); - $("span[id$='repo-list-time-down'], span[id$='repo-list-time-up']").on('click',sort_lib_by_time); - } - }); -}); diff --git a/seahub/templates/view_file_base.html b/seahub/templates/view_file_base.html index d23fd50688..f8e6894ce2 100644 --- a/seahub/templates/view_file_base.html +++ b/seahub/templates/view_file_base.html @@ -170,15 +170,16 @@ $(function() { $('#share').click(function() { var op = $(this), name = "{{filename|escapejs}}", - path = "{{path|escapejs}}"; + path = "{{path|escapejs}}", aj_data = { 'repo_id': "{{ repo.id }}", - 'p': path, - 'type': 'f' + 'path': path }, type = 'f', cur_path = path.substr(0, path.length - name.length); + showSharePopup(op, name, aj_data, type, cur_path); + return false; }); diff --git a/seahub/test_utils.py b/seahub/test_utils.py index 59b9ecd671..73d8f4e7e3 100644 --- a/seahub/test_utils.py +++ b/seahub/test_utils.py @@ -11,7 +11,7 @@ from django.utils.importlib import import_module from exam.decorators import fixture from exam.cases import Exam import seaserv -from seaserv import seafile_api, ccnet_threaded_rpc +from seaserv import seafile_api, ccnet_threaded_rpc, ccnet_api from seahub.base.accounts import User from seahub.utils import mkstemp @@ -139,6 +139,48 @@ class Fixtures(Exam): group_id = self.group.id return ccnet_threaded_rpc.remove_group(group_id, self.user.username) + def set_user_folder_r_permission_to_admin(self): + + # share user's repo to admin with 'rw' permission + seafile_api.share_repo(self.repo.id, self.user.username, + self.admin.username, 'rw') + + # set user sub-folder 'r' permisson to admin + seafile_api.add_folder_user_perm(self.repo.id, + self.folder, 'r', self.admin.username) + + # admin can visit user sub-folder with 'r' permission + assert seafile_api.check_permission_by_path(self.repo.id, + self.folder, self.admin.username) == 'r' + + def set_user_folder_rw_permission_to_admin(self): + + # share user's repo to admin with 'r' permission + seafile_api.share_repo(self.repo.id, self.user.username, + self.admin.username, 'r') + + # set user sub-folder 'rw' permisson to admin + seafile_api.add_folder_user_perm(self.repo.id, + self.folder, 'rw', self.admin.username) + + # admin can visit user sub-folder with 'rw' permission + assert seafile_api.check_permission_by_path(self.repo.id, + self.folder, self.admin.username) == 'rw' + + def share_repo_to_group_with_r_permission(self): + seafile_api.set_group_repo( + self.repo.id, self.group.id, self.user.username, 'r') + + def share_repo_to_group_with_rw_permission(self): + seafile_api.set_group_repo( + self.repo.id, self.group.id, self.user.username, 'rw') + + def add_admin_to_group(self): + ccnet_api.group_add_member( + self.group.id, self.user.username, self.admin.username) + + assert ccnet_api.is_group_user(self.group.id, self.admin.username) + class BaseTestCase(TestCase, Fixtures): def tearDown(self): diff --git a/seahub/urls.py b/seahub/urls.py index 88d6b1ae9c..053c6a2816 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -20,6 +20,8 @@ from seahub.views.ajax import * from seahub.api2.endpoints.groups import Groups, Group from seahub.api2.endpoints.group_members import GroupMembers, GroupMembersBulk, GroupMember from seahub.api2.endpoints.share_links import ShareLinks, ShareLink +from seahub.api2.endpoints.shared_folders import SharedFolders +from seahub.api2.endpoints.shared_repos import SharedRepos, SharedRepo from seahub.api2.endpoints.upload_links import UploadLinks, UploadLink from seahub.api2.endpoints.file import FileView from seahub.api2.endpoints.dir import DirView @@ -76,7 +78,6 @@ urlpatterns = patterns( url(r'^repo/history/revert/(?P[-0-9a-f]{36})/$', repo_revert_history, name='repo_revert_history'), (r'^repo/upload_check/$', validate_filename), - url(r'^repo/unsetinnerpub/(?P[-0-9a-f]{36})/$', unsetinnerpub, name='unsetinnerpub'), url(r'^repo/download_dir/(?P[-0-9a-f]{36})/$', repo_download_dir, name='repo_download_dir'), url(r'^repo/file_revisions/(?P[-0-9a-f]{36})/$', file_revisions, name='file_revisions'), url(r'^repo/file-access/(?P[-0-9a-f]{36})/$', file_access, name='file_access'), @@ -177,6 +178,9 @@ urlpatterns = patterns( url(r'^api/v2.1/groups/(?P\d+)/members/$', GroupMembers.as_view(), name='api-v2.1-group-members'), url(r'^api/v2.1/groups/(?P\d+)/members/bulk/$', GroupMembersBulk.as_view(), name='api-v2.1-group-members-bulk'), url(r'^api/v2.1/groups/(?P\d+)/members/(?P[^/]+)/$', GroupMember.as_view(), name='api-v2.1-group-member'), + url(r'^api/v2.1/shared-folders/$', SharedFolders.as_view(), name='api-v2.1-shared-folders'), + url(r'^api/v2.1/shared-repos/$', SharedRepos.as_view(), name='api-v2.1-shared-repos'), + url(r'^api/v2.1/shared-repos/(?P[-0-9a-f]{36})/$', SharedRepo.as_view(), name='api-v2.1-shared-repo'), url(r'^api/v2.1/share-links/$', ShareLinks.as_view(), name='api-v2.1-share-links'), url(r'^api/v2.1/share-links/(?P[a-f0-9]{10})/$', ShareLink.as_view(), name='api-v2.1-share-link'), url(r'^api/v2.1/upload-links/$', UploadLinks.as_view(), name='api-v2.1-upload-links'), diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py index 3d40d4eda0..434ceeff3e 100644 --- a/seahub/views/__init__.py +++ b/seahub/views/__init__.py @@ -825,80 +825,6 @@ def libraries(request): 'can_add_pub_repo': can_add_pub_repo, }, context_instance=RequestContext(request)) -@login_required -@require_POST -def unsetinnerpub(request, repo_id): - """Unshare repos in organization or in share admin page. - - Only system admin, organization admin or repo owner can perform this op. - """ - repo = get_repo(repo_id) - perm = request.GET.get('permission', None) - if perm is None: - return render_error(request, _(u'Argument is not valid')) - if not repo: - messages.error(request, _('Failed to unshare the library, as it does not exist.')) - return HttpResponseRedirect(reverse('share_admin')) - - # permission check - username = request.user.username - if is_org_context(request): - org_id = request.user.org.org_id - repo_owner = seafile_api.get_org_repo_owner(repo.id) - is_repo_owner = True if repo_owner == username else False - if not (request.user.org.is_staff or is_repo_owner): - raise Http404 - else: - repo_owner = seafile_api.get_repo_owner(repo.id) - is_repo_owner = True if repo_owner == username else False - if not (request.user.is_staff or is_repo_owner): - raise Http404 - - try: - if is_org_context(request): - org_id = request.user.org.org_id - seaserv.seafserv_threaded_rpc.unset_org_inner_pub_repo(org_id, - repo.id) - else: - seaserv.unset_inner_pub_repo(repo.id) - - origin_repo_id, origin_path = get_origin_repo_info(repo.id) - if origin_repo_id is not None: - perm_repo_id = origin_repo_id - perm_path = origin_path - else: - perm_repo_id = repo.id - perm_path = '/' - - send_perm_audit_msg('delete-repo-perm', username, 'all', - perm_repo_id, perm_path, perm) - - messages.success(request, _('Unshare "%s" successfully.') % repo.name) - except SearpcError: - messages.error(request, _('Failed to unshare "%s".') % repo.name) - - referer = request.META.get('HTTP_REFERER', None) - next = settings.SITE_ROOT if referer is None else referer - - return HttpResponseRedirect(next) - -# @login_required -# def ownerhome(request, owner_name): -# owned_repos = [] -# quota_usage = 0 - -# owned_repos = seafserv_threaded_rpc.list_owned_repos(owner_name) -# quota_usage = seafserv_threaded_rpc.get_user_quota_usage(owner_name) - -# user_dict = user_info(request, owner_name) - -# return render_to_response('ownerhome.html', { -# "owned_repos": owned_repos, -# "quota_usage": quota_usage, -# "owner": owner_name, -# "user_dict": user_dict, -# }, context_instance=RequestContext(request)) - @login_required def repo_set_access_property(request, repo_id): ap = request.GET.get('ap', '') diff --git a/static/scripts/app/collections/share-admin-folders.js b/static/scripts/app/collections/share-admin-folders.js new file mode 100644 index 0000000000..136067586e --- /dev/null +++ b/static/scripts/app/collections/share-admin-folders.js @@ -0,0 +1,20 @@ +define([ + 'underscore', + 'backbone', + 'common', + 'app/models/share-admin-folder' +], function(_, Backbone, Common, ShareAdminFolder) { + 'use strict'; + + var ShareAdminFolderCollection = Backbone.Collection.extend({ + + model: ShareAdminFolder, + + url: function() { + return Common.getUrl({name: 'share_admin_folders'}); + } + + }); + + return ShareAdminFolderCollection; +}); diff --git a/static/scripts/app/collections/share-admin-repos.js b/static/scripts/app/collections/share-admin-repos.js new file mode 100644 index 0000000000..5e8ceb6aec --- /dev/null +++ b/static/scripts/app/collections/share-admin-repos.js @@ -0,0 +1,20 @@ +define([ + 'underscore', + 'backbone', + 'common', + 'app/models/share-admin-repo' +], function(_, Backbone, Common, ShareAdminRepo) { + 'use strict'; + + var ShareAdminRepoCollection = Backbone.Collection.extend({ + + model: ShareAdminRepo, + + url: function() { + return Common.getUrl({name: 'share_admin_repos'}); + } + + }); + + return ShareAdminRepoCollection; +}); diff --git a/static/scripts/app/collections/share-admin-share-links.js b/static/scripts/app/collections/share-admin-share-links.js new file mode 100644 index 0000000000..f27bc1e180 --- /dev/null +++ b/static/scripts/app/collections/share-admin-share-links.js @@ -0,0 +1,20 @@ +define([ + 'underscore', + 'backbone', + 'common', + 'app/models/share-admin-share-link' +], function(_, Backbone, Common, ShareAdminShareLink) { + 'use strict'; + + var ShareAdminShareLinkCollection = Backbone.Collection.extend({ + + model: ShareAdminShareLink, + + url: function() { + return Common.getUrl({name: 'share_admin_share_links'}); + } + + }); + + return ShareAdminShareLinkCollection; +}); diff --git a/static/scripts/app/collections/share-admin-upload-links.js b/static/scripts/app/collections/share-admin-upload-links.js new file mode 100644 index 0000000000..e8f1b55508 --- /dev/null +++ b/static/scripts/app/collections/share-admin-upload-links.js @@ -0,0 +1,20 @@ +define([ + 'underscore', + 'backbone', + 'common', + 'app/models/share-admin-upload-link' +], function(_, Backbone, Common, ShareAdminUploadLink) { + 'use strict'; + + var ShareAdminUploadLinkCollection = Backbone.Collection.extend({ + + model: ShareAdminUploadLink, + + url: function() { + return Common.getUrl({name: 'share_admin_upload_links'}); + } + + }); + + return ShareAdminUploadLinkCollection; +}); diff --git a/static/scripts/app/models/share-admin-folder.js b/static/scripts/app/models/share-admin-folder.js new file mode 100644 index 0000000000..73f1f80171 --- /dev/null +++ b/static/scripts/app/models/share-admin-folder.js @@ -0,0 +1,35 @@ +define([ + 'underscore', + 'backbone', + 'common' +], function(_, Backbone, Common) { + 'use strict'; + + var ShareAdminFolder = Backbone.Model.extend({ + + getWebUrl: function() { + var path = this.get('path'), + repo_id = this.get('repo_id'); + + return "#my-libs/lib/" + repo_id + Common.encodePath(path); + }, + + getIconUrl: function(size) { + var is_readonly = this.get('share_permission') == "r" ? true : false; + return Common.getDirIconUrl(is_readonly, size); + }, + + getIconTitle: function() { + var icon_title = ''; + if (this.get('share_permission') == "rw") { + icon_title = gettext("Read-Write"); + } else { + icon_title = gettext("Read-Only"); + } + return icon_title; + } + + }); + + return ShareAdminFolder; +}); diff --git a/static/scripts/app/models/share-admin-repo.js b/static/scripts/app/models/share-admin-repo.js new file mode 100644 index 0000000000..7d0dca4d3c --- /dev/null +++ b/static/scripts/app/models/share-admin-repo.js @@ -0,0 +1,33 @@ +define([ + 'underscore', + 'backbone', + 'common' +], function(_, Backbone, Common) { + 'use strict'; + + var ShareAdminRepo = Backbone.Model.extend({ + + getWebUrl: function() { + return "#common/lib/" + this.get('repo_id') + "/"; + }, + + getIconUrl: function(size) { + var is_readonly = this.get('share_permission') == "r" ? true : false; + return Common.getLibIconUrl(false, is_readonly, size); + }, + + getIconTitle: function() { + var icon_title = ''; + if (this.get('share_permission') == "rw") { + icon_title = gettext("Read-Write"); + } else { + icon_title = gettext("Read-Only"); + } + + return icon_title; + } + + }); + + return ShareAdminRepo; +}); diff --git a/static/scripts/app/models/share-admin-share-link.js b/static/scripts/app/models/share-admin-share-link.js new file mode 100644 index 0000000000..9172e754fc --- /dev/null +++ b/static/scripts/app/models/share-admin-share-link.js @@ -0,0 +1,45 @@ +define([ + 'underscore', + 'backbone', + 'common', + 'moment' +], function(_, Backbone, Common, Moment) { + 'use strict'; + + var ShareAdminShareLink = Backbone.Model.extend({ + parse: function(response) { + var attrs = _.clone(response), + expire_date = response.expire_date; + + if (expire_date) { + attrs.expire_date_timestamp = Moment(expire_date).format('X'); + } else { + attrs.expire_date_timestamp = 0; + } + + return attrs; + }, + + getIconUrl: function(size) { + if (this.get('is_dir')) { + return Common.getDirIconUrl(false, size); + } else { + return Common.getFileIconUrl(this.get('obj_name'), size); + } + }, + + getWebUrl: function() { + var repo_id = this.get('repo_id'); + var dirent_path = this.get('path'); + + if (this.get('is_dir')) { + return "#common/lib/" + repo_id + Common.encodePath(dirent_path); + } else { + return app.config.siteRoot + "lib/" + repo_id + "/file" + Common.encodePath(dirent_path); + } + } + + }); + + return ShareAdminShareLink; +}); diff --git a/static/scripts/app/models/share-admin-upload-link.js b/static/scripts/app/models/share-admin-upload-link.js new file mode 100644 index 0000000000..c08b3f6e52 --- /dev/null +++ b/static/scripts/app/models/share-admin-upload-link.js @@ -0,0 +1,21 @@ +define([ + 'underscore', + 'backbone', + 'common' +], function(_, Backbone, Common) { + 'use strict'; + + var ShareAdminUploadLink = Backbone.Model.extend({ + getIconUrl: function(size) { + return Common.getDirIconUrl(false , size); + }, + + getWebUrl: function() { + var repo_id = this.get('repo_id'); + var dirent_path = this.get('path'); + return "#common/lib/" + repo_id + Common.encodePath(dirent_path); + } + }); + + return ShareAdminUploadLink; +}); diff --git a/static/scripts/app/router.js b/static/scripts/app/router.js index 6e79cbae03..dbd656d4b8 100644 --- a/static/scripts/app/router.js +++ b/static/scripts/app/router.js @@ -11,14 +11,19 @@ define([ 'app/views/organization', 'app/views/dir', 'app/views/starred-file', - 'app/views/devices', 'app/views/activities', + 'app/views/devices', + 'app/views/share-admin-repos', + 'app/views/share-admin-folders', + 'app/views/share-admin-share-links', + 'app/views/share-admin-upload-links', 'app/views/notifications', 'app/views/account' ], function($, Backbone, Common, SideNavView, MyReposView, - SharedReposView, GroupsView, GroupView, - OrgView, DirView, StarredFileView, DevicesView, ActivitiesView, - NotificationsView, AccountView) { + SharedReposView, GroupsView, GroupView, OrgView, DirView, + StarredFileView, ActivitiesView, DevicesView, ShareAdminReposView, + ShareAdminFoldersView, ShareAdminShareLinksView, + ShareAdminUploadLinksView, NotificationsView, AccountView) { "use strict"; var Router = Backbone.Router.extend({ @@ -38,6 +43,10 @@ define([ 'starred/': 'showStarredFile', 'activities/': 'showActivities', 'devices/': 'showDevices', + 'share-admin-libs/': 'showShareAdminRepos', + 'share-admin-folders/': 'showShareAdminFolders', + 'share-admin-share-links/': 'showShareAdminShareLinks', + 'share-admin-upload-links/': 'showShareAdminUploadLinks', // Default '*actions': 'showRepos' }, @@ -62,6 +71,10 @@ define([ this.starredFileView = new StarredFileView(); this.devicesView = new DevicesView(); this.activitiesView = new ActivitiesView(); + this.shareAdminReposView = new ShareAdminReposView(); + this.shareAdminFoldersView = new ShareAdminFoldersView(); + this.shareAdminShareLinksView = new ShareAdminShareLinksView(); + this.shareAdminUploadLinksView = new ShareAdminUploadLinksView(); app.ui.notificationsView = this.notificationsView = new NotificationsView(); app.ui.accountView = this.accountView = new AccountView(); @@ -204,16 +217,40 @@ define([ this.sideNavView.setCurTab('starred'); }, + showActivities: function() { + this.switchCurrentView(this.activitiesView); + this.activitiesView.show(); + this.sideNavView.setCurTab('activities'); + }, + showDevices: function() { this.switchCurrentView(this.devicesView); this.devicesView.show(); this.sideNavView.setCurTab('devices'); }, - showActivities: function() { - this.switchCurrentView(this.activitiesView); - this.activitiesView.show(); - this.sideNavView.setCurTab('activities'); + showShareAdminRepos: function() { + this.switchCurrentView(this.shareAdminReposView); + this.shareAdminReposView.show(); + this.sideNavView.setCurTab('share-admin-repos'); + }, + + showShareAdminFolders: function() { + this.switchCurrentView(this.shareAdminFoldersView); + this.shareAdminFoldersView.show(); + this.sideNavView.setCurTab('share-admin-folders'); + }, + + showShareAdminShareLinks: function() { + this.switchCurrentView(this.shareAdminShareLinksView); + this.shareAdminShareLinksView.show(); + this.sideNavView.setCurTab('share-admin-links'); + }, + + showShareAdminUploadLinks: function() { + this.switchCurrentView(this.shareAdminUploadLinksView); + this.shareAdminUploadLinksView.show(); + this.sideNavView.setCurTab('share-admin-links'); } }); diff --git a/static/scripts/app/views/group.js b/static/scripts/app/views/group.js index 44174c7c61..e1466da5b4 100644 --- a/static/scripts/app/views/group.js +++ b/static/scripts/app/views/group.js @@ -170,19 +170,22 @@ define([ sortByName: function() { this.$('.by-time .sort-icon').hide(); var repos = this.repos; - var el = this.$('.by-name .sort-icon'); - repos.comparator = function(a, b) { // a, b: model - var result = Common.compareTwoWord(a.get('name'), b.get('name')); - if (el.hasClass('icon-caret-up')) { + var $el = this.$('.by-name .sort-icon'); + if ($el.hasClass('icon-caret-up')) { + repos.comparator = function(a, b) { // a, b: model + var result = Common.compareTwoWord(a.get('name'), b.get('name')); return -result; - } else { + }; + } else { + repos.comparator = function(a, b) { // a, b: model + var result = Common.compareTwoWord(a.get('name'), b.get('name')); return result; - } - }; + }; + } repos.sort(); this.$tableBody.empty(); repos.each(this.addOne, this); - el.toggleClass('icon-caret-up icon-caret-down').show(); + $el.toggleClass('icon-caret-up icon-caret-down').show(); repos.comparator = null; return false; }, @@ -190,18 +193,20 @@ define([ sortByTime: function() { this.$('.by-name .sort-icon').hide(); var repos = this.repos; - var el = this.$('.by-time .sort-icon'); - repos.comparator = function(a, b) { // a, b: model - if (el.hasClass('icon-caret-down')) { + var $el = this.$('.by-time .sort-icon'); + if ($el.hasClass('icon-caret-down')) { + repos.comparator = function(a, b) { // a, b: model return a.get('mtime') < b.get('mtime') ? 1 : -1; - } else { + }; + } else { + repos.comparator = function(a, b) { // a, b: model return a.get('mtime') < b.get('mtime') ? -1 : 1; - } - }; + }; + } repos.sort(); this.$tableBody.empty(); repos.each(this.addOne, this); - el.toggleClass('icon-caret-up icon-caret-down').show(); + $el.toggleClass('icon-caret-up icon-caret-down').show(); repos.comparator = null; return false; }, diff --git a/static/scripts/app/views/myhome-repos.js b/static/scripts/app/views/myhome-repos.js index dc030078f1..24407452f7 100644 --- a/static/scripts/app/views/myhome-repos.js +++ b/static/scripts/app/views/myhome-repos.js @@ -117,19 +117,22 @@ define([ sortByName: function() { $('.by-time .sort-icon').hide(); var repos = this.repos; - var el = $('.by-name .sort-icon', this.$table); - repos.comparator = function(a, b) { // a, b: model - var result = Common.compareTwoWord(a.get('name'), b.get('name')); - if (el.hasClass('icon-caret-up')) { + var $el = $('.by-name .sort-icon', this.$table); + if ($el.hasClass('icon-caret-up')) { + repos.comparator = function(a, b) { // a, b: model + var result = Common.compareTwoWord(a.get('name'), b.get('name')); return -result; - } else { + }; + } else { + repos.comparator = function(a, b) { // a, b: model + var result = Common.compareTwoWord(a.get('name'), b.get('name')); return result; - } - }; + }; + } repos.sort(); this.$tableBody.empty(); repos.each(this.addOne, this); - el.toggleClass('icon-caret-up icon-caret-down').show(); + $el.toggleClass('icon-caret-up icon-caret-down').show(); repos.comparator = null; return false; }, @@ -137,18 +140,20 @@ define([ sortByTime: function() { $('.by-name .sort-icon').hide(); var repos = this.repos; - var el = $('.by-time .sort-icon', this.$table); - repos.comparator = function(a, b) { // a, b: model - if (el.hasClass('icon-caret-down')) { + var $el = $('.by-time .sort-icon', this.$table); + if ($el.hasClass('icon-caret-down')) { + repos.comparator = function(a, b) { // a, b: model return a.get('mtime') < b.get('mtime') ? 1 : -1; - } else { + }; + } else { + repos.comparator = function(a, b) { // a, b: model return a.get('mtime') < b.get('mtime') ? -1 : 1; - } - }; + }; + } repos.sort(); this.$tableBody.empty(); repos.each(this.addOne, this); - el.toggleClass('icon-caret-up icon-caret-down').show(); + $el.toggleClass('icon-caret-up icon-caret-down').show(); repos.comparator = null; return false; } diff --git a/static/scripts/app/views/myhome-shared-repos.js b/static/scripts/app/views/myhome-shared-repos.js index 8417ed5d09..50d7b2dc60 100644 --- a/static/scripts/app/views/myhome-shared-repos.js +++ b/static/scripts/app/views/myhome-shared-repos.js @@ -104,19 +104,22 @@ define([ sortByName: function() { $('.by-time .sort-icon', this.$table).hide(); var repos = this.repos; - var el = $('.by-name .sort-icon', this.$table); - repos.comparator = function(a, b) { // a, b: model - var result = Common.compareTwoWord(a.get('name'), b.get('name')); - if (el.hasClass('icon-caret-up')) { + var $el = $('.by-name .sort-icon', this.$table); + if ($el.hasClass('icon-caret-up')) { + repos.comparator = function(a, b) { // a, b: model + var result = Common.compareTwoWord(a.get('name'), b.get('name')); return -result; - } else { + }; + } else { + repos.comparator = function(a, b) { // a, b: model + var result = Common.compareTwoWord(a.get('name'), b.get('name')); return result; - } - }; + }; + } repos.sort(); this.$tableBody.empty(); repos.each(this.addOne, this); - el.toggleClass('icon-caret-up icon-caret-down').show(); + $el.toggleClass('icon-caret-up icon-caret-down').show(); repos.comparator = null; return false; }, @@ -124,18 +127,20 @@ define([ sortByTime: function() { $('.by-name .sort-icon', this.$table).hide(); var repos = this.repos; - var el = $('.by-time .sort-icon', this.$table); - repos.comparator = function(a, b) { // a, b: model - if (el.hasClass('icon-caret-down')) { + var $el = $('.by-time .sort-icon', this.$table); + if ($el.hasClass('icon-caret-down')) { + repos.comparator = function(a, b) { // a, b: model return a.get('mtime') < b.get('mtime') ? 1 : -1; - } else { + }; + } else { + repos.comparator = function(a, b) { // a, b: model return a.get('mtime') < b.get('mtime') ? -1 : 1; - } - }; + }; + } repos.sort(); this.$tableBody.empty(); repos.each(this.addOne, this); - el.toggleClass('icon-caret-up icon-caret-down').show(); + $el.toggleClass('icon-caret-up icon-caret-down').show(); repos.comparator = null; return false; } diff --git a/static/scripts/app/views/organization.js b/static/scripts/app/views/organization.js index cde923e2c9..dddc918aef 100644 --- a/static/scripts/app/views/organization.js +++ b/static/scripts/app/views/organization.js @@ -125,19 +125,22 @@ define([ sortByName: function() { $('.by-time .sort-icon', this.$table).hide(); var repos = this.repos; - var el = $('.by-name .sort-icon', this.$table); - repos.comparator = function(a, b) { // a, b: model - var result = Common.compareTwoWord(a.get('name'), b.get('name')); - if (el.hasClass('icon-caret-up')) { + var $el = $('.by-name .sort-icon', this.$table); + if ($el.hasClass('icon-caret-up')) { + repos.comparator = function(a, b) { // a, b: model + var result = Common.compareTwoWord(a.get('name'), b.get('name')); return -result; - } else { + }; + } else { + repos.comparator = function(a, b) { // a, b: model + var result = Common.compareTwoWord(a.get('name'), b.get('name')); return result; - } - }; + }; + } repos.sort(); this.$tableBody.empty(); repos.each(this.addOne, this); - el.toggleClass('icon-caret-up icon-caret-down').show(); + $el.toggleClass('icon-caret-up icon-caret-down').show(); repos.comparator = null; return false; }, @@ -145,18 +148,20 @@ define([ sortByTime: function() { $('.by-name .sort-icon', this.$table).hide(); var repos = this.repos; - var el = $('.by-time .sort-icon', this.$table); - repos.comparator = function(a, b) { // a, b: model - if (el.hasClass('icon-caret-down')) { + var $el = $('.by-time .sort-icon', this.$table); + if ($el.hasClass('icon-caret-down')) { + repos.comparator = function(a, b) { // a, b: model return a.get('mtime') < b.get('mtime') ? 1 : -1; - } else { + }; + } else { + repos.comparator = function(a, b) { // a, b: model return a.get('mtime') < b.get('mtime') ? -1 : 1; - } - }; + }; + } repos.sort(); this.$tableBody.empty(); repos.each(this.addOne, this); - el.toggleClass('icon-caret-up icon-caret-down').show(); + $el.toggleClass('icon-caret-up icon-caret-down').show(); repos.comparator = null; return false; } diff --git a/static/scripts/app/views/share-admin-folder.js b/static/scripts/app/views/share-admin-folder.js new file mode 100644 index 0000000000..c9f742a009 --- /dev/null +++ b/static/scripts/app/views/share-admin-folder.js @@ -0,0 +1,121 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'app/views/widgets/hl-item-view' +], function($, _, Backbone, Common, HLItemView) { + 'use strict'; + + var ShareAdminFolderView = HLItemView.extend({ + + tagName: 'tr', + + template: _.template($('#share-admin-folder-tmpl').html()), + + events: { + 'click .perm-edit-icon': 'showPermSelect', + 'change .perm-select': 'updatePermission', + 'click .unshare': 'removeShare' + }, + + initialize: function(option) { + HLItemView.prototype.initialize.call(this); + this.listenTo(this.model, "change", this.render); + }, + + showPermSelect: function() { + this.$el.closest('table') + .find('.perm-select').hide().end() + .find('.cur-perm, .perm-edit-icon').show(); + + this.$('.cur-perm, .perm-edit-icon').hide(); + this.$('.perm-select').show(); + + return false; + }, + + updatePermission: function() { + var _this = this; + var share_type = this.model.get('share_type'); + var perm = this.$('.perm-select').val(); + var url = Common.getUrl({ + name: 'dir_shared_items', + repo_id: this.model.get('repo_id') + }) + '?p=' + encodeURIComponent(this.model.get('path')); + + if (share_type == 'personal') { + url += '&share_type=user&username=' + encodeURIComponent(this.model.get('user_email')); + } else if (share_type == 'group') { + url += '&share_type=group&group_id=' + this.model.get('group_id'); + } + + $.ajax({ + url: url, + method: 'POST', + dataType: 'json', + beforeSend: Common.prepareCSRFToken, + data: { + 'permission': perm + }, + success: function() { + _this.model.set({'share_permission': perm}); + Common.feedback(gettext("Successfully modified permission"), 'success'); + }, + error: function(xhr) { + Common.ajaxErrorHandler(xhr); + } + }); + }, + + removeShare: function() { + var _this = this; + var share_type = this.model.get('share_type'); + var url = Common.getUrl({ + name: 'dir_shared_items', + repo_id: this.model.get('repo_id') + }) + '?p=' + Common.encodePath(this.model.get('path')); + + if (share_type == 'personal') { + url += '&share_type=user&username=' + Common.encodePath(this.model.get('user_email')); + } else if (share_type == 'group') { + url += '&share_type=group&group_id=' + this.model.get('group_id'); + } + + $.ajax({ + url: url, + type: 'DELETE', + beforeSend: Common.prepareCSRFToken, + success: function() { + _this.remove(); + Common.feedback(gettext("Successfully deleted 1 item"), 'success'); + }, + error: function (xhr) { + Common.ajaxErrorHandler(xhr); + } + }); + + return false; + }, + + render: function() { + var obj = this.model.toJSON(), + icon_size = Common.isHiDPI() ? 96 : 24, + icon_url = this.model.getIconUrl(icon_size); + + _.extend(obj, { + 'icon_url': icon_url, + 'icon_title': this.model.getIconTitle(), + 'url': this.model.getWebUrl(), + 'name': this.model.get('folder_name') + }); + + this.$el.html(this.template(obj)); + + return this; + } + + }); + + return ShareAdminFolderView; +}); diff --git a/static/scripts/app/views/share-admin-folders.js b/static/scripts/app/views/share-admin-folders.js new file mode 100644 index 0000000000..4c66154f8c --- /dev/null +++ b/static/scripts/app/views/share-admin-folders.js @@ -0,0 +1,136 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'app/collections/share-admin-folders', + 'app/views/share-admin-folder' +], function($, _, Backbone, Common, ShareAdminFolderCollection, ShareAdminFolderView) { + 'use strict'; + + var ShareAdminFoldersView = Backbone.View.extend({ + + id: 'share-admin-folders', + + template: _.template($('#share-admin-folders-tmpl').html()), + + initialize: function() { + this.folders = new ShareAdminFolderCollection(); + this.listenTo(this.folders, 'add', this.addOne); + this.listenTo(this.folders, 'reset', this.reset); + this.render(); + + var _this = this; + $(document).click(function(e) { + var target = e.target || event.srcElement; + var $select = _this.$('.perm-select:visible'); + if ($select.length && !$select.is(target)) { + $select.hide(); + $select.closest('tr').find('.cur-perm, .perm-edit-icon').show(); + } + }); + }, + + events: { + 'click .by-name': 'sortByName' + }, + + sortByName: function() { + var folders = this.folders; + var $el = this.$sortIcon; + if ($el.hasClass('icon-caret-up')) { + folders.comparator = function(a, b) { // a, b: model + var result = Common.compareTwoWord(a.get('folder_name'), b.get('folder_name')); + return -result; + }; + } else { + folders.comparator = function(a, b) { // a, b: model + var result = Common.compareTwoWord(a.get('folder_name'), b.get('folder_name')); + return result; + }; + } + folders.sort(); + this.$tableBody.empty(); + folders.each(this.addOne, this); + $el.toggleClass('icon-caret-up icon-caret-down').show(); + folders.comparator = null; + return false; + }, + + render: function() { + this.$el.html(this.template()); + this.$table = this.$('table'); + this.$sortIcon = $('.by-name .sort-icon', this.$table); + this.$tableBody = $('tbody', this.$table); + this.$loadingTip = this.$('.loading-tip'); + this.$emptyTip = this.$('.empty-tips'); + }, + + hide: function() { + this.$el.detach(); + this.attached = false; + }, + + show: function() { + if (!this.attached) { + this.attached = true; + $("#right-panel").html(this.$el); + } + this.showContent(); + }, + + showContent: function() { + this.initPage(); + this.folders.fetch({ + cache: false, + reset: true, + error: function(collection, response, opts) { + _this.$loadingTip.hide(); + var $error = _this.$('.error'); + var err_msg; + if (response.responseText) { + if (response['status'] == 401 || response['status'] == 403) { + err_msg = gettext("Permission error"); + } else { + err_msg = gettext("Error"); + } + } else { + err_msg = gettext('Please check the network.'); + } + $error.html(err_msg).show(); + } + }); + }, + + initPage: function() { + this.$table.hide(); + this.$sortIcon.attr('class', 'sort-icon icon-caret-down').hide(); + this.$tableBody.empty(); + this.$loadingTip.show(); + this.$emptyTip.hide(); + this.$('.error').hide(); + }, + + reset: function() { + this.$('.error').hide(); + this.$loadingTip.hide(); + if (this.folders.length) { + this.$emptyTip.hide(); + this.$tableBody.empty(); + this.folders.each(this.addOne, this); + this.$table.show(); + } else { + this.$table.hide(); + this.$emptyTip.show(); + } + }, + + addOne: function(folder) { + var view = new ShareAdminFolderView({model: folder}); + this.$tableBody.append(view.render().el); + } + + }); + + return ShareAdminFoldersView; +}); diff --git a/static/scripts/app/views/share-admin-repo.js b/static/scripts/app/views/share-admin-repo.js new file mode 100644 index 0000000000..9c848a48af --- /dev/null +++ b/static/scripts/app/views/share-admin-repo.js @@ -0,0 +1,124 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'app/views/widgets/hl-item-view' +], function($, _, Backbone, Common, HLItemView) { + 'use strict'; + + var ShareAdminRepoView = HLItemView.extend({ + + tagName: 'tr', + + template: _.template($('#share-admin-folder-tmpl').html()), + + events: { + 'click .perm-edit-icon': 'showPermSelect', + 'change .perm-select': 'updatePermission', + 'click .unshare': 'removeShare' + }, + + initialize: function(option) { + HLItemView.prototype.initialize.call(this); + this.listenTo(this.model, "change", this.render); + }, + + showPermSelect: function() { + this.$el.closest('table') + .find('.perm-select').hide().end() + .find('.cur-perm, .perm-edit-icon').show(); + + this.$('.cur-perm, .perm-edit-icon').hide(); + this.$('.perm-select').show(); + + return false; + }, + + updatePermission: function() { + var _this = this; + var url = Common.getUrl({ + 'name': 'share_admin_repo', + 'repo_id': this.model.get('repo_id') + }); + var share_type = this.model.get('share_type'); + var perm = this.$('.perm-select').val(); + var data = { + 'share_type': share_type, + 'permission': perm + }; + if (share_type == 'personal') { + data['user'] = this.model.get('user_email'); + } else if (share_type == 'group') { + data['group_id'] = this.model.get('group_id'); + } + + $.ajax({ + url: url, + method: 'PUT', + cache: false, + dataType: 'json', + data: data, + beforeSend: Common.prepareCSRFToken, + success: function() { + _this.model.set({'share_permission': perm}); + Common.feedback(gettext("Successfully modified permission"), 'success'); + }, + error: function(xhr) { + Common.ajaxErrorHandler(xhr); + } + }); + }, + + removeShare: function() { + var _this = this; + var share_type = this.model.get('share_type'); + var url = Common.getUrl({ + 'name': 'share_admin_repo', + 'repo_id': this.model.get('repo_id') + }); + + if (share_type == 'personal') { + url += '?share_type=personal&user=' + encodeURIComponent(this.model.get('user_email')); + } else if (share_type == 'group') { + url += '?share_type=group&group_id=' + this.model.get('group_id'); + } else if (share_type == 'public') { + url += '?share_type=public'; + } + + $.ajax({ + url: url, + type: 'DELETE', + beforeSend: Common.prepareCSRFToken, + success: function() { + _this.remove(); + Common.feedback(gettext("Successfully deleted 1 item"), 'success'); + }, + error: function(xhr) { + Common.ajaxErrorHandler(xhr); + } + }); + + return false; + }, + + render: function() { + var obj = this.model.toJSON(), + icon_size = Common.isHiDPI() ? 96 : 24, + icon_url = this.model.getIconUrl(icon_size); + + _.extend(obj, { + 'icon_url': icon_url, + 'icon_title': this.model.getIconTitle(), + 'url': this.model.getWebUrl(), + 'name': this.model.get('repo_name') + }); + + this.$el.html(this.template(obj)); + return this; + } + + }); + + return ShareAdminRepoView; +}); diff --git a/static/scripts/app/views/share-admin-repos.js b/static/scripts/app/views/share-admin-repos.js new file mode 100644 index 0000000000..63d94fec4a --- /dev/null +++ b/static/scripts/app/views/share-admin-repos.js @@ -0,0 +1,138 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'app/collections/share-admin-repos', + 'app/views/share-admin-repo' +], function($, _, Backbone, Common, ShareAdminRepoCollection, ShareAdminRepoView) { + 'use strict'; + + var ShareAdminReposView = Backbone.View.extend({ + + id: 'share-admin-repos', + + template: _.template($('#share-admin-repos-tmpl').html()), + + initialize: function() { + this.repos = new ShareAdminRepoCollection(); + this.listenTo(this.repos, 'add', this.addOne); + this.listenTo(this.repos, 'reset', this.reset); + this.render(); + + var _this = this; + $(document).click(function(e) { + var target = e.target || event.srcElement; + var $select = _this.$('.perm-select:visible'); + if ($select.length && !$select.is(target)) { + $select.hide(); + $select.closest('tr').find('.cur-perm, .perm-edit-icon').show(); + } + }); + }, + + events: { + 'click .by-name': 'sortByName' + }, + + sortByName: function() { + var repos = this.repos; + var $el = this.$sortIcon; + if ($el.hasClass('icon-caret-up')) { + repos.comparator = function(a, b) { // a, b: model + var result = Common.compareTwoWord(a.get('repo_name'), b.get('repo_name')); + return -result; + }; + } else { + repos.comparator = function(a, b) { // a, b: model + var result = Common.compareTwoWord(a.get('repo_name'), b.get('repo_name')); + return result; + }; + } + repos.sort(); + this.$tableBody.empty(); + repos.each(this.addOne, this); + $el.toggleClass('icon-caret-up icon-caret-down').show(); + repos.comparator = null; + return false; + }, + + render: function() { + this.$el.html(this.template()); + this.$table = this.$('table'); + this.$sortIcon = $('.by-name .sort-icon', this.$table); + this.$tableBody = $('tbody', this.$table); + this.$loadingTip = this.$('.loading-tip'); + this.$emptyTip = this.$('.empty-tips'); + + }, + + hide: function() { + this.$el.detach(); + this.attached = false; + }, + + show: function() { + if (!this.attached) { + this.attached = true; + $("#right-panel").html(this.$el); + } + this.showContent(); + }, + + showContent: function() { + var _this = this; + this.initPage(); + this.repos.fetch({ + cache: false, + reset: true, + error: function(collection, response, opts) { + _this.$loadingTip.hide(); + var $error = _this.$('.error'); + var err_msg; + if (response.responseText) { + if (response['status'] == 401 || response['status'] == 403) { + err_msg = gettext("Permission error"); + } else { + err_msg = gettext("Error"); + } + } else { + err_msg = gettext('Please check the network.'); + } + $error.html(err_msg).show(); + } + }); + }, + + initPage: function() { + this.$table.hide(); + this.$sortIcon.attr('class', 'sort-icon icon-caret-down').hide(); + this.$tableBody.empty(); + this.$loadingTip.show(); + this.$emptyTip.hide(); + this.$('.error').hide(); + }, + + reset: function() { + this.$('.error').hide(); + this.$loadingTip.hide(); + if (this.repos.length) { + this.$emptyTip.hide(); + this.$tableBody.empty(); + this.repos.each(this.addOne, this); + this.$table.show(); + } else { + this.$table.hide(); + this.$emptyTip.show(); + } + }, + + addOne: function(repo) { + var view = new ShareAdminRepoView({model: repo}); + this.$tableBody.append(view.render().el); + } + + }); + + return ShareAdminReposView; +}); diff --git a/static/scripts/app/views/share-admin-share-link.js b/static/scripts/app/views/share-admin-share-link.js new file mode 100644 index 0000000000..7006fd7548 --- /dev/null +++ b/static/scripts/app/views/share-admin-share-link.js @@ -0,0 +1,83 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'moment', + 'app/views/widgets/hl-item-view' +], function($, _, Backbone, Common, Moment, HLItemView) { + 'use strict'; + + var ShareAdminShareLinkView = HLItemView.extend({ + + tagName: 'tr', + + template: _.template($('#share-admin-download-link-tmpl').html()), + linkPopupTemplate: _.template($('#share-admin-link-popup-tmpl').html()), + + events: { + 'click .rm-link': 'removeLink', + 'click .view-link': 'viewLink' + }, + + initialize: function(option) { + HLItemView.prototype.initialize.call(this); + }, + + viewLink: function() { + var $popup = $(this.linkPopupTemplate({'link': this.model.get('link')})); + $popup.modal({focus:false}); + $('#simplemodal-container').css({'width':'auto', 'height':'auto'}); + + var $p = $('p', $popup), + $input = $('input', $popup); + $input.css({'width': $p.width() + 2}); + $p.hide(); + $input.show(); + $input.click(function() { + $(this).select(); + }); + return false; + }, + + removeLink: function() { + var _this = this; + + $.ajax({ + url: Common.getUrl({ + 'name': 'share_admin_share_link', + 'token': this.model.get('token') + }), + type: 'DELETE', + beforeSend: Common.prepareCSRFToken, + success: function() { + _this.remove(); + Common.feedback(gettext("Successfully deleted 1 item"), 'success'); + }, + error: function(xhr) { + Common.ajaxErrorHandler(xhr); + } + }); + + return false; + }, + + render: function() { + var data = this.model.toJSON(); + var icon_size = Common.isHiDPI() ? 96 : 24; + var icon_url = this.model.getIconUrl(icon_size); + + _.extend(data, { + 'icon_url': icon_url, + 'dirent_url': this.model.getWebUrl(), + 'time': data['expire_date'] ? Moment(data['expire_date']).format('YYYY-MM-DD') : '' + }); + + this.$el.html(this.template(data)); + return this; + } + + }); + + return ShareAdminShareLinkView; +}); diff --git a/static/scripts/app/views/share-admin-share-links.js b/static/scripts/app/views/share-admin-share-links.js new file mode 100644 index 0000000000..db550d5452 --- /dev/null +++ b/static/scripts/app/views/share-admin-share-links.js @@ -0,0 +1,189 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'app/collections/share-admin-share-links', + 'app/views/share-admin-share-link' +], function($, _, Backbone, Common, ShareAdminShareLinkCollection, + ShareAdminShareLinkView) { + + 'use strict'; + + var ShareAdminShareLinksView = Backbone.View.extend({ + + id: 'share-admin-download-links', + + template: _.template($('#share-admin-download-links-tmpl').html()), + + initialize: function() { + this.links = new ShareAdminShareLinkCollection(); + this.listenTo(this.links, 'add', this.addOne); + this.listenTo(this.links, 'reset', this.reset); + this.render(); + }, + + events: { + 'click .by-name': 'sortByName', + 'click .by-time': 'sortByTime' + }, + + // initialSort: dirs come first + initialSort: function(a, b) { // a, b: model + var a_is_dir = a.get('is_dir'), + b_is_dir = b.get('is_dir'); + if (a_is_dir && !b_is_dir) { + return -1; + } else if (!a_is_dir && b_is_dir) { + return 1; + } else { + return 0; + } + }, + + sortByName: function() { + var _this = this; + var links = this.links; + var $el = this.$sortByNameIcon; + this.$sortByTimeIcon.hide(); + if ($el.hasClass('icon-caret-up')) { + links.comparator = function(a, b) { // a, b: model + var initialResult = _this.initialSort(a, b); + if (initialResult != 0) { + return initialResult; + } else { + var result = Common.compareTwoWord(a.get('obj_name'), b.get('obj_name')); + return -result; + } + }; + } else { + links.comparator = function(a, b) { // a, b: model + var initialResult = _this.initialSort(a, b); + if (initialResult != 0) { + return initialResult; + } else { + var result = Common.compareTwoWord(a.get('obj_name'), b.get('obj_name')); + return result; + } + }; + } + links.sort(); + this.$tableBody.empty(); + links.each(this.addOne, this); + $el.toggleClass('icon-caret-up icon-caret-down').show(); + links.comparator = null; + return false; + }, + + sortByTime: function() { + var _this = this; + var links = this.links; + var $el = this.$sortByTimeIcon; + this.$sortByNameIcon.hide(); + if ($el.hasClass('icon-caret-down')) { + links.comparator = function(a, b) { // a, b: model + var initialResult = _this.initialSort(a, b); + if (initialResult != 0) { + return initialResult; + } else { + return a.get('expire_date_timestamp') < b.get('expire_date_timestamp') ? 1 : -1; + } + }; + } else { + links.comparator = function(a, b) { // a, b: model + var initialResult = _this.initialSort(a, b); + if (initialResult != 0) { + return initialResult; + } else { + return a.get('expire_date_timestamp') < b.get('expire_date_timestamp') ? -1 : 1; + } + }; + } + links.sort(); + this.$tableBody.empty(); + links.each(this.addOne, this); + $el.toggleClass('icon-caret-up icon-caret-down').show(); + links.comparator = null; + return false; + }, + + render: function() { + this.$el.html(this.template()); + this.$table = this.$('table'); + this.$sortByNameIcon = this.$('.by-name .sort-icon'); + this.$sortByTimeIcon = this.$('.by-time .sort-icon'); + this.$tableBody = $('tbody', this.$table); + this.$loadingTip = this.$('.loading-tip'); + this.$emptyTip = this.$('.empty-tips'); + }, + + hide: function() { + this.$el.detach(); + this.attached = false; + }, + + show: function() { + if (!this.attached) { + this.attached = true; + $("#right-panel").html(this.$el); + } + this.showContent(); + }, + + showContent: function() { + var _this = this; + this.initPage(); + this.links.fetch({ + cache: false, + reset: true, + error: function(collection, response, opts) { + _this.$loadingTip.hide(); + var $error = _this.$('.error'); + var err_msg; + if (response.responseText) { + if (response['status'] == 401 || response['status'] == 403) { + err_msg = gettext("Permission error"); + } else { + err_msg = gettext("Error"); + } + } else { + err_msg = gettext('Please check the network.'); + } + $error.html(err_msg).show(); + } + }); + }, + + initPage: function() { + this.$table.hide(); + this.$sortByNameIcon.attr('class', 'sort-icon icon-caret-up').show(); + this.$sortByTimeIcon.attr('class', 'sort-icon icon-caret-down').hide(); + this.$tableBody.empty(); + this.$loadingTip.show(); + this.$emptyTip.hide(); + this.$('.error').hide(); + }, + + reset: function() { + this.$('.error').hide(); + this.$loadingTip.hide(); + if (this.links.length) { + this.$emptyTip.hide(); + this.$tableBody.empty(); + this.links.each(this.addOne, this); + this.$table.show(); + } else { + this.$emptyTip.show(); + this.$table.hide(); + } + }, + + addOne: function(link) { + var view = new ShareAdminShareLinkView({model: link}); + this.$tableBody.append(view.render().el); + } + + }); + + return ShareAdminShareLinksView; +}); diff --git a/static/scripts/app/views/share-admin-upload-link.js b/static/scripts/app/views/share-admin-upload-link.js new file mode 100644 index 0000000000..cf6c5f84f7 --- /dev/null +++ b/static/scripts/app/views/share-admin-upload-link.js @@ -0,0 +1,81 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'app/views/widgets/hl-item-view' +], function($, _, Backbone, Common, HLItemView) { + 'use strict'; + + var ShareAdminUploadLinkView = HLItemView.extend({ + + tagName: 'tr', + + template: _.template($('#share-admin-upload-link-tmpl').html()), + linkPopupTemplate: _.template($('#share-admin-link-popup-tmpl').html()), + + events: { + 'click .rm-link': 'removeLink', + 'click .view-link': 'viewLink' + }, + + initialize: function(option) { + HLItemView.prototype.initialize.call(this); + }, + + viewLink: function() { + var $popup = $(this.linkPopupTemplate({'link': this.model.get('link')})); + $popup.modal({focus:false}); + $('#simplemodal-container').css({'width':'auto', 'height':'auto'}); + + var $p = $('p', $popup), + $input = $('input', $popup); + $input.css({'width': $p.width() + 2}); + $p.hide(); + $input.show(); + $input.click(function() { + $(this).select(); + }); + return false; + }, + + removeLink: function() { + var _this = this; + + $.ajax({ + url: Common.getUrl({ + 'name': 'share_admin_upload_link', + 'token': this.model.get('token') + }), + type: 'DELETE', + beforeSend: Common.prepareCSRFToken, + success: function() { + _this.remove(); + Common.feedback(gettext("Successfully deleted 1 item"), 'success'); + }, + error: function(xhr) { + Common.ajaxErrorHandler(xhr); + } + }); + + return false; + }, + + render: function() { + var data = this.model.toJSON(); + var icon_size = Common.isHiDPI() ? 96 : 24; + var icon_url = this.model.getIconUrl(icon_size); + + _.extend(data, { + 'icon_url': icon_url, + 'dirent_url': this.model.getWebUrl() + }); + + this.$el.html(this.template(data)); + return this; + } + + }); + + return ShareAdminUploadLinkView; +}); diff --git a/static/scripts/app/views/share-admin-upload-links.js b/static/scripts/app/views/share-admin-upload-links.js new file mode 100644 index 0000000000..405539bc50 --- /dev/null +++ b/static/scripts/app/views/share-admin-upload-links.js @@ -0,0 +1,101 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'app/collections/share-admin-upload-links', + 'app/views/share-admin-upload-link' +], function($, _, Backbone, Common, ShareAdminUploadLinkCollection, + ShareAdminUploadLinkView) { + + 'use strict'; + + var ShareAdminUploadLinksView = Backbone.View.extend({ + + id: 'share-admin-upload-links', + + template: _.template($('#share-admin-upload-links-tmpl').html()), + + initialize: function() { + this.links = new ShareAdminUploadLinkCollection(); + this.listenTo(this.links, 'add', this.addOne); + this.listenTo(this.links, 'reset', this.reset); + this.render(); + }, + + render: function() { + this.$el.html(this.template()); + this.$table = this.$('table'); + this.$tableBody = $('tbody', this.$table); + this.$loadingTip = this.$('.loading-tip'); + this.$emptyTip = this.$('.empty-tips'); + }, + + hide: function() { + this.$el.detach(); + this.attached = false; + }, + + show: function() { + if (!this.attached) { + this.attached = true; + $("#right-panel").html(this.$el); + } + this.showContent(); + }, + + showContent: function() { + var _this = this; + this.initPage(); + this.links.fetch({ + cache: false, + reset: true, + error: function(collection, response, opts) { + _this.$loadingTip.hide(); + var $error = _this.$('.error'); + var err_msg; + if (response.responseText) { + if (response['status'] == 401 || response['status'] == 403) { + err_msg = gettext("Permission error"); + } else { + err_msg = gettext("Error"); + } + } else { + err_msg = gettext('Please check the network.'); + } + $error.html(err_msg).show(); + } + }); + }, + + initPage: function() { + this.$table.hide(); + this.$tableBody.empty(); + this.$loadingTip.show(); + this.$emptyTip.hide(); + this.$('.error').hide(); + }, + + reset: function() { + this.$('.error').hide(); + this.$loadingTip.hide(); + if (this.links.length) { + this.$emptyTip.hide(); + this.$tableBody.empty(); + this.links.each(this.addOne, this); + this.$table.show(); + } else { + this.$emptyTip.show(); + this.$table.hide(); + } + }, + + addOne: function(link) { + var view = new ShareAdminUploadLinkView({model: link}); + this.$tableBody.append(view.render().el); + } + + }); + + return ShareAdminUploadLinksView; +}); diff --git a/static/scripts/app/views/share.js b/static/scripts/app/views/share.js index 91bcad2f42..a3e01d0a59 100644 --- a/static/scripts/app/views/share.js +++ b/static/scripts/app/views/share.js @@ -105,16 +105,18 @@ define([ downloadLinkPanelInit: function() { var _this = this; - var after_op_success = function(data) { + var after_op_success = function(data) { // data is [] or [{...}] _this.$('.loading-tip').hide(); - if (data['download_link']) { - _this.download_link = data["download_link"]; // for 'link send' - _this.download_link_token = data["token"]; // for 'link delete' - _this.$('#download-link').html(data['download_link']); - _this.$('#direct-dl-link').html(data['download_link']+'?raw=1'); - if (data['is_expired']) { + if (data.length == 1) { + var link_data = data[0], + link = link_data.link; + _this.download_link = link; // for 'link send' + _this.download_link_token = link_data.token; // for 'link delete' + _this.$('#download-link').html(link); + _this.$('#direct-dl-link').html(link + '?raw=1'); + if (link_data.is_expired) { _this.$('#send-download-link').addClass('hide'); _this.$('#download-link, #direct-dl-link').append(' (' + gettext('Expired') + ')'); } @@ -125,11 +127,10 @@ define([ }; // check if downloadLink exists Common.ajaxGet({ - 'get_url': Common.getUrl({name: 'get_shared_download_link'}), + 'get_url': Common.getUrl({name: 'share_admin_share_links'}), 'data': { 'repo_id': this.repo_id, - 'p': this.dirent_path, - 'type': this.is_dir ? 'd' : 'f' + 'path': this.dirent_path }, 'after_op_success': after_op_success }); @@ -239,13 +240,8 @@ define([ $.extend(post_data, { 'repo_id': this.repo_id, - 'p': this.dirent_path + 'path': this.dirent_path }); - if (link_type == 'download') { - $.extend(post_data, { - 'type': this.is_dir? 'd' : 'f' - }); - } var _this = this; var after_op_success = function(data) { @@ -269,14 +265,14 @@ define([ } if (link_type == 'download') { - _this.$('#download-link').html(data["download_link"]); // TODO: add 'click & select' func - _this.$('#direct-dl-link').html(data['download_link'] + '?raw=1'); - _this.download_link = data["download_link"]; // for 'link send' + _this.$('#download-link').html(data["link"]); // TODO: add 'click & select' func + _this.$('#direct-dl-link').html(data['link'] + '?raw=1'); + _this.download_link = data["link"]; // for 'link send' _this.download_link_token = data["token"]; // for 'link delete' _this.$('#download-link-operations').removeClass('hide'); } else { - _this.$('#upload-link').html(data["upload_link"]); - _this.upload_link = data["upload_link"]; + _this.$('#upload-link').html(data["link"]); + _this.upload_link = data["link"]; _this.upload_link_token = data["token"]; _this.$('#upload-link-operations').removeClass('hide'); } @@ -295,7 +291,7 @@ define([ this.generateLink({ link_type: 'download', form: this.$('#generate-download-link-form'), - post_url: Common.getUrl({name: 'get_shared_download_link'}) + post_url: Common.getUrl({name: 'share_admin_share_links'}) }); return false; }, @@ -384,9 +380,12 @@ define([ deleteDownloadLink: function() { var _this = this; $.ajax({ - url: Common.getUrl({name: 'delete_shared_download_link'}), - type: 'POST', - data: { 't': this.download_link_token }, + url: Common.getUrl({ + 'name': 'share_admin_share_link', + 'token': this.download_link_token + }), + type: 'DELETE', + cache: false, beforeSend: Common.prepareCSRFToken, dataType: 'json', success: function(data) { @@ -398,11 +397,13 @@ define([ uploadLinkPanelInit: function() { var _this = this; - var after_op_success = function(data) { - if (data['upload_link']) { - _this.upload_link_token = data["token"]; - _this.upload_link = data["upload_link"]; - _this.$('#upload-link').html(data["upload_link"]); // TODO + var after_op_success = function(data) { // data is [] or [{...}] + if (data.length == 1) { + var link_data = data[0], + link = link_data.link; + _this.upload_link_token = link_data.token; + _this.upload_link = link; + _this.$('#upload-link').html(link); _this.$('#upload-link-operations').removeClass('hide'); } else { _this.$('#generate-upload-link-form').removeClass('hide'); @@ -410,8 +411,11 @@ define([ }; // check if upload link exists Common.ajaxGet({ - 'get_url': Common.getUrl({name: 'get_share_upload_link'}), // TODO - 'data': {'repo_id': this.repo_id, 'p': this.dirent_path}, + 'get_url': Common.getUrl({name: 'share_admin_upload_links'}), + 'data': { + 'repo_id': this.repo_id, + 'path': this.dirent_path + }, 'after_op_success': after_op_success }); }, @@ -428,7 +432,7 @@ define([ this.generateLink({ link_type: 'upload', form: this.$('#generate-upload-link-form'), - post_url: Common.getUrl({name: 'get_share_upload_link'}) + post_url: Common.getUrl({name: 'share_admin_upload_links'}) }); return false; }, @@ -458,9 +462,12 @@ define([ deleteUploadLink: function() { var _this = this; $.ajax({ - url: Common.getUrl({name: 'delete_shared_upload_link'}), - type: 'POST', - data: { 't': this.upload_link_token }, + url: Common.getUrl({ + 'name': 'share_admin_upload_link', + 'token': this.upload_link_token + }), + type: 'DELETE', + cache: false, beforeSend: Common.prepareCSRFToken, dataType: 'json', success: function(data) { diff --git a/static/scripts/common.js b/static/scripts/common.js index 4d2087dce3..b6e3e89790 100644 --- a/static/scripts/common.js +++ b/static/scripts/common.js @@ -119,17 +119,22 @@ define([ case 'repo_group_folder_perm': return siteRoot + 'api2/repos/' + options.repo_id + '/group-folder-perm/'; case 'repo_change_password': return siteRoot + 'ajax/repo/' + options.repo_id + '/setting/change-passwd/'; + // Share admin + case 'share_admin_repos': return siteRoot + 'api/v2.1/shared-repos/'; + case 'share_admin_repo': return siteRoot + 'api/v2.1/shared-repos/' + options.repo_id + '/'; + case 'share_admin_folders': return siteRoot + 'api/v2.1/shared-folders/'; + case 'share_admin_share_links': return siteRoot + 'api/v2.1/share-links/'; + case 'share_admin_share_link': return siteRoot + 'api/v2.1/share-links/' + options.token + '/'; + case 'share_admin_upload_links': return siteRoot + 'api/v2.1/upload-links/'; + case 'share_admin_upload_link': return siteRoot + 'api/v2.1/upload-links/' + options.token + '/'; + // Permission case 'set_user_folder_perm': return siteRoot + 'ajax/repo/' + options.repo_id + '/set-user-folder-perm/'; case 'set_group_folder_perm': return siteRoot + 'ajax/repo/' + options.repo_id + '/set-group-folder-perm/'; // Links - case 'get_shared_download_link': return siteRoot + 'share/ajax/get-download-link/'; - case 'delete_shared_download_link': return siteRoot + 'share/ajax/link/remove/'; case 'send_shared_download_link': return siteRoot + 'share/link/send/'; case 'send_shared_upload_link': return siteRoot + 'share/upload_link/send/'; - case 'delete_shared_upload_link': return siteRoot + 'share/ajax/upload_link/remove/'; - case 'get_share_upload_link': return siteRoot + 'share/ajax/get-upload-link/'; // Group case 'groups': return siteRoot + 'api/v2.1/groups/'; @@ -157,6 +162,7 @@ define([ case 'search_user': return siteRoot + 'api2/search-user/'; case 'user_profile': return siteRoot + 'profile/' + options.username + '/'; case 'space_and_traffic': return siteRoot + 'ajax/space_and_traffic/'; + // sysadmin case 'sysinfo': return siteRoot + 'api/v2.1/admin/sysinfo/'; case 'admin-devices': return siteRoot + 'api/v2.1/admin/devices/'; diff --git a/tests/api/endpoints/test_share_links.py b/tests/api/endpoints/test_share_links.py index acb6829a43..3eb9ec5dc9 100644 --- a/tests/api/endpoints/test_share_links.py +++ b/tests/api/endpoints/test_share_links.py @@ -7,6 +7,11 @@ from seahub.test_utils import BaseTestCase from seahub.share.models import FileShare from seahub.api2.endpoints.share_links import ShareLinks, ShareLink +try: + from seahub.settings import LOCAL_PRO_DEV_ENV +except ImportError: + LOCAL_PRO_DEV_ENV = False + class ShareLinksTest(BaseTestCase): def setUp(self): @@ -34,7 +39,6 @@ class ShareLinksTest(BaseTestCase): link = FileShare.objects.get(token=token) link.delete() - # test file share link def test_get_file_share_link(self): self.login_as(self.user) token = self._add_file_share_link() @@ -55,34 +59,6 @@ class ShareLinksTest(BaseTestCase): self._remove_share_link(token) - def test_create_file_share_link(self): - self.login_as(self.user) - - resp = self.client.post(self.url, {'path': self.file_path, 'repo_id': self.repo_id}) - self.assertEqual(200, resp.status_code) - - json_resp = json.loads(resp.content) - assert json_resp['link'] is not None - assert json_resp['token'] is not None - assert json_resp['is_expired'] is not None - - assert json_resp['token'] in json_resp['link'] - assert 'f' in json_resp['link'] - - self._remove_share_link(json_resp['token']) - - def test_delete_file_share_link(self): - self.login_as(self.user) - token = self._add_file_share_link() - - url = reverse('api-v2.1-share-link', args=[token]) - resp = self.client.delete(url, {}, 'application/x-www-form-urlencoded') - self.assertEqual(200, resp.status_code) - - json_resp = json.loads(resp.content) - assert json_resp['success'] is True - - # test dir share link def test_get_dir_share_link(self): self.login_as(self.user) token = self._add_dir_share_link() @@ -103,6 +79,30 @@ class ShareLinksTest(BaseTestCase): self._remove_share_link(token) + @patch.object(ShareLinks, '_can_generate_shared_link') + def test_get_link_with_invalid_user_role_permission(self, mock_can_generate_shared_link): + self.login_as(self.user) + mock_can_generate_shared_link.return_value = False + + resp = self.client.get(self.url) + self.assertEqual(403, resp.status_code) + + def test_create_file_share_link(self): + self.login_as(self.user) + + resp = self.client.post(self.url, {'path': self.file_path, 'repo_id': self.repo_id}) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert json_resp['link'] is not None + assert json_resp['token'] is not None + assert json_resp['is_expired'] is not None + + assert json_resp['token'] in json_resp['link'] + assert 'f' in json_resp['link'] + + self._remove_share_link(json_resp['token']) + def test_create_dir_share_link(self): self.login_as(self.user) @@ -119,6 +119,80 @@ class ShareLinksTest(BaseTestCase): self._remove_share_link(json_resp['token']) + def test_create_link_with_invalid_repo_permission(self): + # login with admin to create share link in user repo + self.login_as(self.admin) + data = {'path': self.file_path, 'repo_id': self.repo_id} + resp = self.client.post(self.url, data) + self.assertEqual(403, resp.status_code) + + def test_create_link_with_rw_permission_folder(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.set_user_folder_rw_permission_to_admin() + + # login with admin to create share link for 'r' permission folder + self.login_as(self.admin) + data = {'path': self.file_path, 'repo_id': self.repo_id} + resp = self.client.post(self.url, data) + self.assertEqual(200, resp.status_code) + + def test_create_link_with_rw_permission_folder_in_group(self): + + self.share_repo_to_group_with_rw_permission() + self.add_admin_to_group() + + # login with admin to create share link for 'r' permission folder + self.login_as(self.admin) + data = {'path': self.file_path, 'repo_id': self.repo_id} + resp = self.client.post(self.url, data) + self.assertEqual(200, resp.status_code) + + def test_create_link_with_r_permission_folder(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.set_user_folder_r_permission_to_admin() + + # login with admin to create share link for 'r' permission folder + self.login_as(self.admin) + data = {'path': self.file_path, 'repo_id': self.repo_id} + resp = self.client.post(self.url, data) + self.assertEqual(200, resp.status_code) + + def test_create_link_with_r_permission_folder_in_group(self): + + self.share_repo_to_group_with_r_permission() + self.add_admin_to_group() + + # login with admin to create share link for 'r' permission folder + self.login_as(self.admin) + data = {'path': self.file_path, 'repo_id': self.repo_id} + resp = self.client.post(self.url, data) + self.assertEqual(200, resp.status_code) + + @patch.object(ShareLinks, '_can_generate_shared_link') + def test_create_link_with_invalid_urer_role_permission(self, mock_can_generate_shared_link): + self.login_as(self.user) + mock_can_generate_shared_link.return_value = False + + resp = self.client.post(self.url, {'path': self.folder_path, 'repo_id': self.repo_id}) + self.assertEqual(403, resp.status_code) + + def test_delete_file_share_link(self): + self.login_as(self.user) + token = self._add_file_share_link() + + url = reverse('api-v2.1-share-link', args=[token]) + resp = self.client.delete(url, {}, 'application/x-www-form-urlencoded') + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert json_resp['success'] is True + def test_delete_dir_share_link(self): self.login_as(self.user) token = self._add_file_share_link() @@ -129,42 +203,20 @@ class ShareLinksTest(BaseTestCase): json_resp = json.loads(resp.content) assert json_resp['success'] is True - # test permission - def test_can_not_delete_link_if_not_owner(self): + def test_delete_link_if_not_owner(self): self.login_as(self.admin) token = self._add_file_share_link() url = reverse('api-v2.1-share-link', args=[token]) resp = self.client.delete(url, {}, 'application/x-www-form-urlencoded') self.assertEqual(403, resp.status_code) - @patch.object(ShareLinks, '_can_generate_shared_link') - def test_can_not_get_and_create_link_with_invalid_permission(self, mock_can_generate_shared_link): - self.login_as(self.user) - mock_can_generate_shared_link.return_value = False - - resp = self.client.get(self.url) - self.assertEqual(403, resp.status_code) - - resp = self.client.post(self.url) - self.assertEqual(403, resp.status_code) - - self.logout() - - # login with another user to test repo permission - self.login_as(self.admin) - mock_can_generate_shared_link.return_value = True - - args = '?repo_id=%s' % self.repo_id - resp = self.client.get(self.url + args) - self.assertEqual(403, resp.status_code) - - data = {'path': self.file_path, 'repo_id': self.repo_id} - resp = self.client.post(self.url, data) - self.assertEqual(403, resp.status_code) - @patch.object(ShareLink, '_can_generate_shared_link') - def test_can_not_delete_link_with_invalid_permission(self, mock_can_generate_shared_link): + def test_delete_link_with_invalid_user_repo_permission(self, mock_can_generate_shared_link): token = self._add_file_share_link() + + self.login_as(self.user) + mock_can_generate_shared_link.return_value = False + url = reverse('api-v2.1-share-link', args=[token]) resp = self.client.delete(url, {}, 'application/x-www-form-urlencoded') self.assertEqual(403, resp.status_code) diff --git a/tests/api/endpoints/test_shared_folders.py b/tests/api/endpoints/test_shared_folders.py new file mode 100644 index 0000000000..0beb306491 --- /dev/null +++ b/tests/api/endpoints/test_shared_folders.py @@ -0,0 +1,60 @@ +import os +import json + +from django.core.urlresolvers import reverse + +from seaserv import seafile_api + +from seahub.test_utils import BaseTestCase + +class SharedFoldersTest(BaseTestCase): + + def create_virtual_repo(self): + + name = os.path.basename(self.folder.rstrip('/')) + sub_repo_id = seafile_api.create_virtual_repo( + self.repo.id, self.folder, name, + name, self.user.username) + return sub_repo_id + + def share_repo_to_user(self, repo_id): + seafile_api.share_repo( + repo_id, self.user.username, + self.admin.username, 'rw') + + def share_repo_to_group(self, repo_id): + seafile_api.set_group_repo( + repo_id, self.group.id, + self.user.username, 'rw') + + def setUp(self): + self.repo_id = self.repo.id + self.group_id = self.group.id + self.user_name = self.user.username + self.admin_user = self.admin.username + self.url = reverse('api-v2.1-shared-folders') + + sub_repo_id = self.create_virtual_repo() + self.share_repo_to_user(sub_repo_id) + self.share_repo_to_group(sub_repo_id) + + def tearDown(self): + self.remove_repo() + + def test_can_get(self): + self.login_as(self.user) + resp = self.client.get(self.url) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert json_resp[0]['share_type'] == 'personal' + assert json_resp[1]['share_type'] == 'group' + + def test_get_with_invalid_repo_permission(self): + # login with admin, then get user's share repo info + self.login_as(self.admin) + resp = self.client.get(self.url) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert len(json_resp) == 0 diff --git a/tests/api/endpoints/test_shared_repos.py b/tests/api/endpoints/test_shared_repos.py new file mode 100644 index 0000000000..5acc8e99dd --- /dev/null +++ b/tests/api/endpoints/test_shared_repos.py @@ -0,0 +1,210 @@ +import json + +from django.core.urlresolvers import reverse + +import seaserv +from seaserv import seafile_api + +from seahub.test_utils import BaseTestCase + +class SharedReposTest(BaseTestCase): + + def share_repo_to_user(self): + seafile_api.share_repo( + self.repo.id, self.user.username, + self.admin.username, 'rw') + + def share_repo_to_group(self): + seafile_api.set_group_repo( + self.repo.id, self.group.id, + self.user.username, 'rw') + + def share_repo_to_public(self): + seafile_api.add_inner_pub_repo( + self.repo.id, 'rw') + + def setUp(self): + self.repo_id = self.repo.id + self.group_id = self.group.id + self.user_name = self.user.username + self.admin_name = self.admin.username + self.url = reverse('api-v2.1-shared-repos') + + def tearDown(self): + self.remove_repo() + + def test_can_get(self): + self.share_repo_to_user() + self.share_repo_to_group() + self.share_repo_to_public() + + self.login_as(self.user) + resp = self.client.get(self.url) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert json_resp[0]['share_type'] == 'personal' + assert json_resp[1]['share_type'] == 'group' + assert json_resp[2]['share_type'] == 'public' + + def test_get_with_invalid_repo_permission(self): + self.share_repo_to_user() + self.share_repo_to_group() + self.share_repo_to_public() + + # login with admin, then get user's share repo info + self.login_as(self.admin) + resp = self.client.get(self.url) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert len(json_resp) == 0 + + def test_can_update_user_share_perm(self): + self.share_repo_to_user() + + assert seafile_api.check_permission_by_path( + self.repo_id, '/', self.admin_name) == 'rw' + + self.login_as(self.user) + + url = reverse('api-v2.1-shared-repo', args=[self.repo_id]) + data = 'permission=r&share_type=personal&user=%s' % self.admin_name + resp = self.client.put(url, data, 'application/x-www-form-urlencoded') + + self.assertEqual(200, resp.status_code) + assert seafile_api.check_permission_by_path( + self.repo_id, '/', self.admin_name) == 'r' + + def test_can_update_group_share_perm(self): + self.share_repo_to_group() + +# print seafile_api.get_folder_group_perm(self.repo_id, '/', int(self.group_id)) + + repos = seafile_api.get_group_repos_by_owner(self.user_name) + assert repos[0].permission == 'rw' + + self.login_as(self.user) + + url = reverse('api-v2.1-shared-repo', args=[self.repo_id]) + data = 'permission=r&share_type=group&group_id=%s' % self.group_id + resp = self.client.put(url, data, 'application/x-www-form-urlencoded') + + self.assertEqual(200, resp.status_code) + + repos = seafile_api.get_group_repos_by_owner(self.user_name) + assert repos[0].permission == 'r' + + def test_can_update_public_share_perm(self): + for r in seaserv.seafserv_threaded_rpc.list_inner_pub_repos(): + seafile_api.remove_inner_pub_repo(r.repo_id) + + self.share_repo_to_public() + + repos = seaserv.seafserv_threaded_rpc.list_inner_pub_repos_by_owner( + self.user_name) + assert repos[0].permission == 'rw' + + self.login_as(self.user) + + url = reverse('api-v2.1-shared-repo', args=[self.repo_id]) + data = 'permission=r&share_type=public' + resp = self.client.put(url, data, 'application/x-www-form-urlencoded') + + self.assertEqual(200, resp.status_code) + + repos = seaserv.seafserv_threaded_rpc.list_inner_pub_repos_by_owner( + self.user_name) + assert repos[0].permission == 'r' + + def test_delete_user_share(self): + self.share_repo_to_user() + + # admin user can view repo + assert seafile_api.check_permission_by_path( + self.repo_id, '/', self.admin_name) == 'rw' + + self.login_as(self.user) + + args = '?share_type=personal&user=%s' % self.admin_name + url = reverse('api-v2.1-shared-repo', args=[self.repo_id]) + args + resp = self.client.delete(url, {}, 'application/x-www-form-urlencoded') + + self.assertEqual(200, resp.status_code) + + # admin user can NOT view repo + assert seafile_api.check_permission_by_path( + self.repo_id, '/', self.admin_name) == None + + def test_delete_group_share(self): + self.share_repo_to_group() + + # repo in group + repos = seafile_api.get_group_repos_by_owner(self.user_name) + assert repos[0].permission == 'rw' + + self.login_as(self.user) + + args = '?share_type=group&group_id=%s' % self.group_id + url = reverse('api-v2.1-shared-repo', args=[self.repo_id]) + args + resp = self.client.delete(url, {}, 'application/x-www-form-urlencoded') + + self.assertEqual(200, resp.status_code) + + # repo NOT in group + repos = seafile_api.get_group_repos_by_owner(self.user_name) + assert len(repos) == 0 + + def test_delete_public_share(self): + for r in seaserv.seafserv_threaded_rpc.list_inner_pub_repos(): + seafile_api.remove_inner_pub_repo(r.repo_id) + + self.share_repo_to_public() + + # repo in public + repos = seaserv.seafserv_threaded_rpc.list_inner_pub_repos_by_owner( + self.user_name) + assert repos[0].permission == 'rw' + + self.login_as(self.user) + + args = '?share_type=public' + url = reverse('api-v2.1-shared-repo', args=[self.repo_id]) + args + resp = self.client.delete(url, {}, 'application/x-www-form-urlencoded') + + self.assertEqual(200, resp.status_code) + + # repo NOT in public + repos = seaserv.seafserv_threaded_rpc.list_inner_pub_repos_by_owner( + self.user_name) + assert len(repos) == 0 + + def test_update_perm_if_not_owner(self): + self.share_repo_to_user() + + # admin can view repo but NOT owner + assert seafile_api.check_permission_by_path( + self.repo_id, '/', self.admin_name) == 'rw' + + self.login_as(self.admin) + + url = reverse('api-v2.1-shared-repo', args=[self.repo_id]) + data = 'permission=r&share_type=personal' + resp = self.client.put(url, data, 'application/x-www-form-urlencoded') + + self.assertEqual(403, resp.status_code) + + def test_delete_perm_if_not_owner(self): + self.share_repo_to_user() + + # admin can view repo but NOT owner + assert seafile_api.check_permission_by_path( + self.repo_id, '/', self.admin_name) == 'rw' + + self.login_as(self.admin) + + args = '?share_type=personal&user=%s' % self.admin_name + url = reverse('api-v2.1-shared-repo', args=[self.repo_id]) + args + resp = self.client.delete(url, {}, 'application/x-www-form-urlencoded') + + self.assertEqual(403, resp.status_code) diff --git a/tests/api/endpoints/test_upload_links.py b/tests/api/endpoints/test_upload_links.py index 30bcce7744..911bd4a050 100644 --- a/tests/api/endpoints/test_upload_links.py +++ b/tests/api/endpoints/test_upload_links.py @@ -7,6 +7,11 @@ from seahub.test_utils import BaseTestCase from seahub.share.models import UploadLinkShare from seahub.api2.endpoints.upload_links import UploadLinks, UploadLink +try: + from seahub.settings import LOCAL_PRO_DEV_ENV +except ImportError: + LOCAL_PRO_DEV_ENV = False + class UploadLinksTest(BaseTestCase): def setUp(self): @@ -46,6 +51,14 @@ class UploadLinksTest(BaseTestCase): self._remove_upload_link(token) + @patch.object(UploadLinks, '_can_generate_shared_link') + def test_get_link_with_invalid_user_role_permission(self, mock_can_generate_shared_link): + self.login_as(self.user) + mock_can_generate_shared_link.return_value = False + + resp = self.client.get(self.url) + self.assertEqual(403, resp.status_code) + def test_create_upload_link(self): self.login_as(self.user) @@ -61,6 +74,66 @@ class UploadLinksTest(BaseTestCase): self._remove_upload_link(json_resp['token']) + @patch.object(UploadLinks, '_can_generate_shared_link') + def test_create_link_with_invalid_user_role_permission(self, mock_can_generate_shared_link): + self.login_as(self.user) + mock_can_generate_shared_link.return_value = False + + resp = self.client.post(self.url, {'path': self.folder_path, 'repo_id': self.repo_id}) + self.assertEqual(403, resp.status_code) + + def test_create_link_with_rw_permission_folder(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.set_user_folder_rw_permission_to_admin() + + # login with admin to create upload link in user's repo + self.login_as(self.admin) + + data = {'path': self.folder_path, 'repo_id': self.repo_id} + resp = self.client.post(self.url, data) + self.assertEqual(200, resp.status_code) + + def test_create_link_with_rw_permission_folder_in_group(self): + + self.share_repo_to_group_with_rw_permission() + self.add_admin_to_group() + + # login with admin to create upload link in user's repo + self.login_as(self.admin) + + data = {'path': self.folder_path, 'repo_id': self.repo_id} + resp = self.client.post(self.url, data) + self.assertEqual(200, resp.status_code) + + def test_can_not_create_link_with_r_permission_folder(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.set_user_folder_r_permission_to_admin() + + # login with admin to create upload link in user's repo + self.login_as(self.admin) + + data = {'path': self.folder_path, 'repo_id': self.repo_id} + resp = self.client.post(self.url, data) + self.assertEqual(403, resp.status_code) + + def test_can_not_create_link_with_r_permission_folder_in_group(self): + + self.share_repo_to_group_with_r_permission() + self.add_admin_to_group() + + # login with admin to create upload link in user's repo + self.login_as(self.admin) + + data = {'path': self.folder_path, 'repo_id': self.repo_id} + resp = self.client.post(self.url, data) + self.assertEqual(403, resp.status_code) + def test_delete_upload_link(self): self.login_as(self.user) token = self._add_upload_link() @@ -72,43 +145,21 @@ class UploadLinksTest(BaseTestCase): json_resp = json.loads(resp.content) assert json_resp['success'] is True - # test permission - def test_can_not_delete_link_if_not_owner(self): - self.login_as(self.admin) + @patch.object(UploadLink, '_can_generate_shared_link') + def test_delete_link_with_invalid_user_role_permission(self, mock_can_generate_shared_link): token = self._add_upload_link() - url = reverse('api-v2.1-upload-link', args=[token]) - resp = self.client.delete(url, {}, 'application/x-www-form-urlencoded') - self.assertEqual(403, resp.status_code) - - @patch.object(UploadLinks, '_can_generate_shared_link') - def test_can_not_get_and_create_link_with_invalid_permission(self, mock_can_generate_shared_link): self.login_as(self.user) mock_can_generate_shared_link.return_value = False - resp = self.client.get(self.url) - self.assertEqual(403, resp.status_code) - - resp = self.client.post(self.url) - self.assertEqual(403, resp.status_code) - - self.logout() - - # login with another user to test repo permission - self.login_as(self.admin) - mock_can_generate_shared_link.return_value = True - - args = '?repo_id=%s' % self.repo_id - resp = self.client.get(self.url + args) - self.assertEqual(403, resp.status_code) - - data = {'path': self.folder_path, 'repo_id': self.repo_id} - resp = self.client.post(self.url, data) - self.assertEqual(403, resp.status_code) - - @patch.object(UploadLink, '_can_generate_shared_link') - def test_can_not_delete_link_with_invalid_permission(self, mock_can_generate_shared_link): - token = self._add_upload_link() + url = reverse('api-v2.1-upload-link', args=[token]) + resp = self.client.delete(url, {}, 'application/x-www-form-urlencoded') + self.assertEqual(403, resp.status_code) + + def test_delete_link_if_not_owner(self): + self.login_as(self.admin) + token = self._add_upload_link() + url = reverse('api-v2.1-upload-link', args=[token]) resp = self.client.delete(url, {}, 'application/x-www-form-urlencoded') self.assertEqual(403, resp.status_code) diff --git a/tests/seahub/share/views/test_ajax_get_download_link.py b/tests/seahub/share/views/test_ajax_get_download_link.py deleted file mode 100644 index 2a8729fd42..0000000000 --- a/tests/seahub/share/views/test_ajax_get_download_link.py +++ /dev/null @@ -1,122 +0,0 @@ -import json - -from django.core.urlresolvers import reverse - -from seahub.share.models import FileShare -from seahub.test_utils import BaseTestCase - - -class AjaxGetDownloadLinkTest(BaseTestCase): - def setUp(self): - - self.url = reverse('ajax_get_download_link') - - self.user_repo_id = self.repo.id - self.user_dir_path = self.folder - self.user_file_path = self.file - - def test_can_generate_file_share_link(self): - self.login_as(self.user) - - url = self.url - data = { - 'repo_id': self.user_repo_id, - 'p': self.user_file_path, - 'type': 'f', - } - resp = self.client.post(url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - json_resp = json.loads(resp.content) - assert '/f/' in json_resp['download_link'] - - def test_can_generate_dir_share_link(self): - self.login_as(self.user) - - url = self.url - data = { - 'repo_id': self.user_repo_id, - 'p': self.user_dir_path, - 'type': 'd', - } - resp = self.client.post(url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - json_resp = json.loads(resp.content) - assert '/d/' in json_resp['download_link'] - - def test_can_get_file_share_link(self): - fs = FileShare.objects.create_file_link(self.user.username, - self.user_repo_id, self.user_file_path) - - self.login_as(self.user) - - args = '?repo_id=%s&p=%s&type=%s' % (self.user_repo_id, self.user_file_path, 'f') - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - json_resp = json.loads(resp.content) - assert fs.token in json_resp['download_link'] - - def test_can_get_dir_share_link(self): - fs = FileShare.objects.create_dir_link(self.user.username, - self.user_repo_id, self.user_dir_path) - - self.login_as(self.user) - - args = '?repo_id=%s&p=%s&type=%s' % (self.user_repo_id, self.user_dir_path, 'd') - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - json_resp = json.loads(resp.content) - assert fs.token in json_resp['download_link'] - - def test_can_not_get_if_not_login(self): - args = '?repo_id=%s&p=%s&type=%s' % (self.user_repo_id, self.user_file_path, 'f') - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(401, resp.status_code) - - def test_invalid_args(self): - self.login_as(self.user) - - # invalid type - args = '?repo_id=%s&p=%s&type=%s' % (self.user_repo_id, self.user_file_path, 'invalid_type') - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(400, resp.status_code) - - # invalid repo_id - args = '?invalid_repo_id=%s&p=%s&type=%s' % (self.user_repo_id, self.user_file_path, 'f') - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(400, resp.status_code) - - # invalid path - args = '?repo_id=%s&invalid_path=%s&type=%s' % (self.user_repo_id, self.user_file_path, 'f') - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(400, resp.status_code) - - def test_invalid_recourse(self): - self.login_as(self.user) - - # invalid repo_id - args = '?repo_id=%s&p=%s&type=%s' % ('invalid repo id', self.user_file_path, 'f') - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(500, resp.status_code) - - # invalid repo_id - args = '?repo_id=%s&p=%s&type=%s' % (self.user_repo_id[:30] + '123456', self.user_file_path, 'f') - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(404, resp.status_code) - - # invalid path - args = '?repo_id=%s&p=%s&type=%s' % (self.user_repo_id, 'invalid path', 'f') - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(404, resp.status_code) - - def test_invalid_permission(self): - self.login_as(self.admin) - - args = '?repo_id=%s&p=%s&type=%s' % (self.user_repo_id, self.user_file_path, 'f') - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(403, resp.status_code) diff --git a/tests/seahub/share/views/test_ajax_get_upload_link.py b/tests/seahub/share/views/test_ajax_get_upload_link.py deleted file mode 100644 index 12f0775509..0000000000 --- a/tests/seahub/share/views/test_ajax_get_upload_link.py +++ /dev/null @@ -1,89 +0,0 @@ -import json - -from django.core.urlresolvers import reverse - -from seahub.share.models import UploadLinkShare -from seahub.test_utils import BaseTestCase - - -class AjaxGetUploadLinkTest(BaseTestCase): - def setUp(self): - self.url = reverse('ajax_get_upload_link') - - self.user_repo_id = self.repo.id - self.user_dir_path = self.folder - self.user_file_path = self.file - - def test_can_generate(self): - self.login_as(self.user) - - url = self.url - data = { - 'repo_id': self.user_repo_id, - 'p': self.user_dir_path, - } - resp = self.client.post(url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - json_resp = json.loads(resp.content) - assert '/u/d/' in json_resp['upload_link'] - - def test_can_get(self): - upload_link = UploadLinkShare.objects.create_upload_link_share( - self.user.username, self.user_repo_id, self.user_dir_path) - - self.login_as(self.user) - - args = '?repo_id=%s&p=%s' % (self.user_repo_id, self.user_dir_path) - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - json_resp = json.loads(resp.content) - assert upload_link.token in json_resp['upload_link'] - - def test_unlogin_user(self): - args = '?repo_id=%s&p=%s' % (self.user_repo_id, self.user_dir_path) - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(401, resp.status_code) - - def test_invalid_args(self): - self.login_as(self.user) - - # invalid repo_id - args = '?invalid_repo_id=%s&p=%s' % (self.user_repo_id, self.user_dir_path) - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(400, resp.status_code) - - # invalid path - args = '?repo_id=%s&invalid_path=%s' % (self.user_repo_id, self.user_dir_path) - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(400, resp.status_code) - - def test_invalid_recourse(self): - self.login_as(self.user) - - # invalid repo_id - args = '?repo_id=%s&p=%s' % ('invalid repo id', self.user_dir_path) - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(500, resp.status_code) - - # invalid repo_id - args = '?repo_id=%s&p=%s' % (self.user_repo_id[:30] + '123456', self.user_dir_path) - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(404, resp.status_code) - - # invalid path - args = '?repo_id=%s&p=%s' % (self.user_repo_id, 'invalid path') - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(404, resp.status_code) - - def test_invalid_permission(self): - self.login_as(self.admin) - - args = '?repo_id=%s&p=%s' % (self.user_repo_id, self.user_dir_path) - url = self.url + args - resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(403, resp.status_code) diff --git a/tests/seahub/share/views/test_list_priv_shared_folders.py b/tests/seahub/share/views/test_list_priv_shared_folders.py deleted file mode 100644 index dc563bb57f..0000000000 --- a/tests/seahub/share/views/test_list_priv_shared_folders.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -import os - -from django.core.urlresolvers import reverse - -from seaserv import seafile_api - -from seahub.test_utils import BaseTestCase -class ListPrivSharedFoldersTest(BaseTestCase): - def tearDown(self): - self.remove_repo() - - def test_can_list_priv_shared_folders(self): - repo_id = self.repo.id - username = self.user.username - - parent_dir = '/' - dirname = 'test-folder' - full_dir_path = os.path.join(parent_dir, dirname) - - # create folder - self.create_folder(repo_id=repo_id, - parent_dir=parent_dir, - dirname=dirname, - username=username) - - sub_repo_id = seafile_api.create_virtual_repo(repo_id, full_dir_path, dirname, dirname, username) - seafile_api.share_repo(sub_repo_id, username, self.admin.username, 'rw') - - self.login_as(self.user) - resp = self.client.get(reverse('list_priv_shared_folders')) - self.assertEqual(200, resp.status_code) - href = reverse("view_common_lib_dir", args=[repo_id, full_dir_path.strip('/')]) - self.assertRegexpMatches(resp.content, href) diff --git a/tests/seahub/share/views/test_shared_links.py b/tests/seahub/share/views/test_shared_links.py deleted file mode 100644 index e9e77a4dc5..0000000000 --- a/tests/seahub/share/views/test_shared_links.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -import os - -from django.test import TestCase -from django.core.urlresolvers import reverse -import requests - -from seahub.share.models import FileShare -from seahub.test_utils import Fixtures, BaseTestCase - -class ListSharedLinksTest(BaseTestCase): - def setUp(self): - share_file_info = { - 'username': 'test@test.com', - 'repo_id': self.repo.id, - 'path': self.file, - 'password': None, - 'expire_date': None, - } - self.fs = FileShare.objects.create_file_link(**share_file_info) - - def tearDown(self): - self.remove_repo() - - def test_can_render(self): - self.login_as(self.user) - - resp = self.client.get(reverse('list_shared_links')) - self.assertEqual(200, resp.status_code) - self.assertTemplateUsed(resp, 'share/links.html') - - def test_can_render_when_parent_dir_of_link_is_removed(self): - """Issue https://github.com/haiwen/seafile/issues/1283 - """ - # create a file in a folder - self.create_file(repo_id=self.repo.id, - parent_dir=self.folder, - filename='file.txt', - username=self.user.username) - # share that file - share_file_info = { - 'username': self.user.username, - 'repo_id': self.repo.id, - 'path': os.path.join(self.folder, 'file.txt'), - 'password': None, - 'expire_date': None, - } - fs = FileShare.objects.create_file_link(**share_file_info) - - self.login_as(self.user) - - resp = self.client.get(reverse('list_shared_links')) - self.assertEqual(200, resp.status_code) - - # then delete parent folder, see whether it raises error - self.remove_folder() - resp = self.client.get(reverse('list_shared_links')) - self.assertEqual(200, resp.status_code)