首页  编辑  

油猴脚本: 查询SharePoint用户

Tags: /计算机文档/软件应用技巧/   Date Created:
在使用SharePoint.com的过程当中,我们常常需要查询某些用户的具体信息,如查看用户名对应的邮箱,方便去公司内部查看对应的人员。因为有重名的用户,光从用户名是无法看出来对应哪个用户的。此外如何想查看某个用户所有的文章,也是很麻烦的事情,本文的插件可以实现相关的功能。

如果要查看详细的SharePoint信息,可以使用 SP Insider 插件:。

利用下面的脚本,可以方便查询对应用户的详细信息:
// ==UserScript==
// @name         SharePoint 用户查询
// @namespace    http://tampermonkey.net/
// @version      1.3
// @license      GPL-3.0-only
// @description  输入关键字查询SharePoint用户详细信息和查看该用户所有文章,方便确认作者身份。按 Ctrl+Q 激活功能。
// @author       Kingron
// @match        https://*.sharepoint.com/sites/*
// @match        https://*.sharepoint.cn/sites/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=sharepoint.com
// @run-at       document-start
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        td.ms-vb2 {
            max-width: 400px !important;
        }
    `);

    GM_addStyle(`
        .a_f_cb6f7c2e:not(.b_f_cb6f7c2e):not(.z_f_cb6f7c2e) {
            margin: 0px 0 !important;
        }
    `);

    GM_addStyle(`
        .ms-list-TitleLink {
            min-width: 400px !important;
        }
    `);

    function convertToCSV(items) {
        if (items.length === 0) return '';

        // 获取表头
        const headers = Object.keys(items[0]);

        // 构建 CSV 内容
        let csv = headers.join(',') + '\r\n';

        items.forEach(item => {
            const row = headers.map(header => {
                let value = item[header] !== undefined ? item[header] : '';

                // 将值转为字符串
                if (value === null || value === undefined || header === 'burnDownChart') {
                    value = '';
                } else if (typeof value === 'object') {
                    value = JSON.stringify(value, null, 2);
                } else {
                    value = String(value);
                }
                // 处理包含特殊字符的情况:逗号、换行符、双引号
                if (value.includes(',') || value.includes('\n') || value.includes('\r') || value.includes('"')) {
                    // 转义双引号(" -> "")
                    value = value.replace(/"/g, '""');
                    // 用双引号包裹整个值
                    value = `"${value}"`;
                }
                return value;
            });
            csv += row.join(',') + '\r\n';
        });

        return csv;
    }

    function downloadCSV(csvData, filename = 'data.csv') {
        const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' });
        const link = document.createElement('a');
        const url = URL.createObjectURL(blob);

        link.setAttribute('href', url);
        link.setAttribute('download', filename);
        link.style.visibility = 'hidden';

        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    }

    // 监听所有资源加载
    (new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
            if (entry.name.includes("SP.UserProfiles.") || entry.name.includes("/users/")) {
                const div = document.querySelector('div[data-automation-id="personaDetails"] div.peopleName');
                if (div.innerText.indexOf(">") > 0) return;
                let mail = entry.name.split("membership%7C")[1]?.split("%27")[0] || "";
                if (mail === "") {
                    mail = entry.name.split("/users/")[1]?.split("?")[0] || ""
                }
                mail = decodeURIComponent(mail);
                console.log("获取到作者信息: ", mail);
                div.innerText += "<" + mail + ">";
                return;
            }
            if (entry.name.includes("/SyncFlowInstances")) {
                getWikiAuthor();
                getRejectInfo();
            }
        });
    })).observe({ entryTypes: ["resource"] });

    function getUserInfo(id) {
        const siteName = getSiteNameFromURL();
        const apiUrl = `/sites/${siteName}/_api/web/GetUserById(${id})`;
        const headers = {
            "Accept": "application/json"
        };

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: apiUrl,
                headers: headers,
                onload: function(response) {
                    const data = JSON.parse(response.responseText);
                    resolve(data);
                },
                onerror: function(error) {
                    reject(error);
                }
            });
        });
    }

    function getWikiAuthor() {
        const siteName = getSiteNameFromURL();
        const apiUrl = `/sites/${siteName}/_api/web/getfilebyserverrelativeurl('${window.location.pathname}')/ListItemAllFields`
        const headers = {
            "Accept": "application/json"
        };

        GM_xmlhttpRequest({
            method: "GET",
            url: apiUrl,
            headers: headers,
            onload: function(response) {
                const data = JSON.parse(response.responseText);
                const user = data.AuthorId;
                getUserInfo(user).then(user => {
                    const element = document.querySelector('h1#pageTitle.ms-core-pageTitle');
                    if (element && user.LoginName && user.Email) {
                        const node = document.createElement('div');
                        element.appendChild(node);
                        node.outerHTML = `<div style="float: right;font-size:20px;position: relative;transform: translateY(50%);" title="${user.LoginName}">Author: ${user.Title}&lt;${user.Email}&gt;</div>`;
                    }
                }).catch(err => {
                    console.error("请求失败:", err);
                });

                const node = document.evaluate("//button[@data-automation-id='pageCommandBarStatus']//span[@class='ms-Button-label ms-ButtonShim-label']", document).iterateNext();
                if (node) {
                    node.addEventListener('click', function() {
                        window.open(data.test_x0020_Approved?.Url, '_blank');
                    });
                }
            },
            onerror: function(error) {
                alert("请求失败!请稍后再试。");
            }
        });
    }

    function getRejectInfo() {
        console.log("获取审批拒绝信息");
        const labelElement = document.evaluate("//button[@data-automation-id='pageCommandBarStatus']//span[@class='ms-Button-label ms-ButtonShim-label' and text()='Rejected']", document).iterateNext();
        if (!labelElement) {
            console.log("未定位到 Reject 节点");
            return;
        }
        let title = document.getElementById('title_text').innerText.trim().replaceAll("-", " ");
        getRejectList().then(data => {
            let firstMatch = data.find(item => item.Title.replaceAll("-", " ").includes(title));
            if (!firstMatch) {
                title = decodeURI(window.location.pathname.split('/').pop()).split(".")[0]?.replaceAll("-", " ");
                firstMatch = data.find(item => item.Title.replaceAll("-", " ").includes(title));
            }
            if (firstMatch) {
                labelElement.title = firstMatch.Body.replaceAll("<br>", "\n").replaceAll("<p>", "").replaceAll("</p>", "").replaceAll("&#58;", ":");
            } else {
                debugger;
                console.log("匹配审批拒绝信息失败");
            }
        });
    }

    function getRejectList() {
        const siteName = getSiteNameFromURL();
        const baseUrl = `/sites/${siteName}/_layouts/15/inplview.aspx?List={3F33E292-D971-4298-99D5-2DABF87EFFBC}&IsXslView=TRUE&IsCSR=TRUE&FilterField1=TaskOutcome&FilterValue1=Rejected`;
        const headers = {
            "Accept": "application/json"
        };

        return new Promise(async (resolve, reject) => {
            try {
                let allData = [];
                let firstRow = 0;
                let hasMoreData = true;

                while (hasMoreData) {
                    const currentUrl = `${baseUrl}&FirstRow=${firstRow}`;
                    const response = await new Promise((innerResolve, innerReject) => {
                        GM_xmlhttpRequest({
                            method: "POST",
                            url: currentUrl,
                            headers: headers,
                            onload: function(response) {
                                innerResolve(response);
                            },
                            onerror: function(error) {
                                innerReject(error);
                            }
                        });
                    });

                    const data = JSON.parse(response.responseText);

                    if (data.Row && data.Row.length > 0) {
                        allData = allData.concat(data.Row);
                    }

                    // Check if we've reached the end of the data
                    if (!data.LastRow || (data.LastRow + 1) < firstRow + data.RowLimit) {
                        hasMoreData = false;
                    } else {
                        firstRow += data.RowLimit; // Move to the next page
                    }
                }

                resolve(allData);
            } catch (error) {
                reject(error);
            }
        });
    }

    function getArticleList(query) {
        const siteName = getSiteNameFromURL();
        const baseUrl = `/sites/${siteName}/_api/web/GetListUsingPath(DecodedUrl='/sites/${siteName}/SitePages')/RenderListDataAsStream?InplaceSearchQuery=${query}`;
        const headers = {
            "Accept": "application/json"
        };

        return new Promise(async (resolve, reject) => {
            try {
                let allData = [];
                let hasMoreData = true;

                let currentUrl = `${baseUrl}&PageFirstRow=1`
                while (hasMoreData) {
                    const response = await new Promise((innerResolve, innerReject) => {
                        GM_xmlhttpRequest({
                            method: "POST",
                            url: currentUrl,
                            headers: headers,
                            onload: function(response) {
                                innerResolve(response);
                            },
                            onerror: function(error) {
                                innerReject(error);
                            }
                        });
                    });

                    const data = JSON.parse(response.responseText);

                    if (data.Row && data.Row.length > 0) {
                        allData = allData.concat(data.Row);
                    }

                    if (data.NextHref) {
                        currentUrl = baseUrl + data.NextHref.replace("?", "&");
                    } else {
                        hasMoreData = false;
                    }
                }

                allData.sort((a, b) => new Date(a.Created) - new Date(b.Created));
                resolve(allData);
            } catch (error) {
                reject(error);
            }
        });
    }

    // 监听 Ctrl+Q 快捷键
    window.addEventListener('keydown', function(event) {
        if (event.ctrlKey && event.key === 'q') {
            event.preventDefault(); // 阻止默认行为

            const keyword = prompt("请输入关键字进行搜索:");
            if (keyword) {
                const siteName = getSiteNameFromURL(); // 获取当前 SharePoint 站点名称
                console.log("SharePoint站点名称: ", siteName);
                if (siteName) {
                    searchUsers(siteName, keyword);
                } else {
                    alert("无法从 URL 获取 SharePoint 站点名!");
                }
            }
        }
    });

    function generateQueryForCurrentQuarter() {
        const now = new Date();
        const year = now.getFullYear();
        const month = now.getMonth();
        const quarter = Math.floor(month / 3);
        const startMonth = quarter * 3;
        const endMonth = startMonth + 2;

        const startDate = new Date(year, startMonth, 1);
        const endDate = new Date(year, endMonth + 1, 0); // 本季度最后一天

        const format = (d) => {
            const yyyy = d.getFullYear();
            const mm = String(d.getMonth() + 1).padStart(2, '0');
            const dd = String(d.getDate()).padStart(2, '0');
            return `${yyyy}-${mm}-${dd}`;
        };

        return `Created:${format(startDate)}..${format(endDate)} AND fileType: aspx`;
    }

    // 监听 Alt+Q 快捷键
    window.addEventListener('keydown', function(event) {
        if (event.altKey && event.key === 'q') {
            event.preventDefault(); // 阻止默认行为

            const keyword = prompt("请输入关键字进行搜索:", generateQueryForCurrentQuarter());
            if (keyword) {
                getArticleList(keyword).then(data => {
                    console.log("获取到文章列表: ", data.length);
                    displayArticles(data);
                });
            }
        }
    });

    // 从当前 URL 中提取 SharePoint 站点名称,支持 .com 和 .cn 域名
    function getSiteNameFromURL() {
        // 优先匹配 sharepoint.com/sites/xxx/bbb/.../SitePages 或 sharepoint.cn/sites/xxx/bbb/.../SitePages
        let regex = /https:\/\/.*\.sharepoint\.(com|cn)\/sites\/(.*)\/(SitePages|_layouts|Lists|Shared%20Documents|SiteAssets|Pages)/;
        let match = window.location.href.match(regex);

        // 如果能匹配到 SitePages 结构,则返回匹配到的 xxx/bbb/... 部分
        if (match) {
            return match[2]; // match[2] 为 sites/后面到 /SitePages 之前的所有内容
        }

        regex = /https:\/\/.*\.sharepoint\.(com|cn)\/sites\/([^\/?]+)/;
        match = window.location.href.match(regex);
        return match ? match[2] : null;
    }

    function copyTable(tableElement) {
        const range = document.createRange();
        const selection = window.getSelection();
        selection.removeAllRanges();
        range.selectNode(tableElement);
        selection.addRange(range);
        try {
            document.execCommand('copy');
            alert('表格已复制到剪切板!');
        } catch (err) {
            alert('复制失败:' + err);
        }
        selection.removeAllRanges();
    }

    // 调用 SharePoint API 获取用户数据
    function searchUsers(siteName, keyword) {
        const apiUrl = `/sites/${siteName}/_api/Web/SiteUsers`;
        const headers = {
            "Accept": "application/json"
        };

        GM_xmlhttpRequest({
            method: "GET",
            url: apiUrl,
            headers: headers,
            onload: function(response) {
                const data = JSON.parse(response.responseText);
                const users = data.value || [];

                // 过滤匹配关键字的用户
                const regex = new RegExp(keyword, 'i'); // 'i' 表示不区分大小写
                const filteredUsers = users.filter(user => {
                    return regex.test(user.Title) || regex.test(user.Email) || regex.test(user.UserPrincipalName);
                });

                if (filteredUsers.length === 0) {
                    alert("没有找到匹配的用户。");
                } else {
                    displayResults(siteName, filteredUsers);
                }
            },
            onerror: function(error) {
                alert("请求失败!请稍后再试。");
            }
        });
    }

    // 在页面上展示搜索结果
    function displayArticles(articles) {
        const container = document.createElement('div');
        container.innerHTML = `
        <div tabindex=0 style="max-width:90%;min-width:800px;width:80%;max-height:70%;position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); overflow:hidden; background-color:#fff; border:1px solid #ccc; border-radius:8px; box-shadow:0 0 10px rgba(0, 0, 0, 0.3); padding:10px; z-index: 9999; display:flex; flex-direction:column;}">
            <div style="flex: 1; overflow-y: auto">
                <style>
                    #_rl_article td { padding: 5px; border-bottom: 1px solid #ddd; }
                    #_rl_article a { text-decoration: none; }
                </style>
                <table id="_rl_article" style="width:100%; border-collapse: collapse; background-color: #fff; border: 1px solid #ddd; border-radius: 5px;">
                    <thead style="position: sticky; top: 0; background-color: #f8f8f8;">
                        <tr>
                            <th>文章</th>
                            <th width=120>作者</th>
                            <th width=120>编辑</th>
                            <th width=140>创建时间</th>
                            <th width=140>编辑时间</th>
                            <th width=50>点赞数</th>
                            <th width=250>提交备注</th>
                            <th width=80>工作流</th>
                            <th width=150>邮件通知</th>
                        </tr>
                    </thead>
                    <tbody>
                        ${articles.map(item => `
                            <tr>
                                <td><a href="${item.FileRef}">${item.FileLeafRef}</a></td>
                                <td><a href="mailto:${item.Author[0]?.email}">${item.Author[0]?.title}</a></td>
                                <td><a href="mailto:${item.Editor[0]?.email}">${item.Editor[0]?.title}</a></td>
                                <td>${item.Created}</td>
                                <td>${item.Modified}</td>
                                <td>${item._LikeCount || ""}</td>
                                <td>${item._CheckinComment}</td>
                                <td><a href="${item.test_x0020_Approved}">${item._ModerationStatus}</a></td>
                                <td>${item.ApproveStatus || ""}</td>
                            </tr>
                        `).join('')}
                    </tbody>
                </table>
            </div>

            <div style="padding-top:10px; display: flex; align-items: center; justify-content: flex-end">按 Esc 关闭&nbsp;&nbsp;
                <div id="divCounter"></div>
                <button id="copyButton" style="padding: 10px 10px; background-color: #3CB371; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px;outline: none;">导出</button>&nbsp;
                <button id="closeButton" style="padding: 10px 10px; background-color: #ff4d4f; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px;outline: none;">关闭</button>
            </div>
        </div>`;
        document.body.appendChild(container);
        document.getElementById("divCounter").innerText = "总数量: " + articles.length + " 篇 ";

        const copyButton = container.querySelector('#copyButton');
        copyButton.addEventListener('click', function() {
            const list = [];
            articles.forEach(user => {
                list.push({
                    文章: user.FileRef,
                    作者: user.Author[0]?.title,
                    作者邮箱: user.Author[0]?.email,
                    编辑: user.Editor[0]?.title,
                    编辑邮箱: user.Editor[0]?.email,
                    创建时间: user.Created,
                    创建时间: user.Modified,
                    点赞数: user._LikeCount || "",
                    提交备注: user._CheckinComment,
                    审批状态: user._ModerationStatus,
                    邮件通知: user.ApproveStatus || "",
                });
            });
            const csvData = convertToCSV(list);
            downloadCSV(csvData);
        });

        const closeButton = container.querySelector('#closeButton');
        closeButton.addEventListener('click', function() {
            document.body.removeChild(container);
        });
        container.addEventListener('keydown', function(event) {
            if (event.key === 'Escape') { document.body.removeChild(container); };
        });
        closeButton.focus();
    }

    // 在页面上展示搜索结果
    function displayResults(site, users) {
        const container = document.createElement('div');

        container.innerHTML = `
        <div tabindex=0 style="max-width:90%; max-height:70%;position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); overflow:hidden; background-color:#fff; border:1px solid #ccc; border-radius:8px; box-shadow:0 0 10px rgba(0, 0, 0, 0.3); padding:10px; z-index: 9999; display:flex; flex-direction:column;}">
            <div style="flex: 1; overflow-y: auto">
                <style>
                    #_rl_table td { padding: 5px; border-bottom: 1px solid #ddd; }
                    #_rl_table a { text-decoration: none; }
                </style>
                <table id="_rl_table" style="width: 100%; border-collapse: collapse; background-color: #fff; border: 1px solid #ddd; border-radius: 5px;">
                    <thead style="position: sticky; top: 0; background-color: #f8f8f8;">
                        <tr>
                            <th>Title</th>
                            <th>Email</th>
                            <th>UserPrincipalName</th>
                            <th>Files</th>
                            <th>Wiki</th>
                        </tr>
                    </thead>
                    <tbody>
                        ${users.map(user => `
                            <tr>
                                <td><a href="/sites/${site}/SitePages/Forms/AllPages.aspx?view=7&q=author:%20*&useFiltersInViewXml=0&viewpath=/sites/${site}/SitePages/Forms/AllPages.aspx&FilterField1=Editor&FilterValue1=${user.Title}&FilterField1=DocIcon&FilterValue1=aspx&FilterType1=Computed&FilterOp1=In&FilterField2=Author&FilterValue2=${user.Title}&FilterType2=User&FilterOp2=In" target="_blank">${user.Title}</a></td>
                                <td><a href="mailto:${user.Email}">${user.Email}</a></td>
                                <td><a href="${user['odata.id']}" target="_blank">${user.UserPrincipalName}</a></td>
                                <td><a href='/sites/${site}/_layouts/15/search.aspx/siteall?oobRefiners={"FileType":["pptx","docx","xlsx","one","pdf","video","html"]}&q=author:${user.Title}&scope=site' target="_blank">List</a></td>
                                <td><a href='/sites/DC3/Wiki/SitePages/Forms/AllPages.aspx?viewid=744ee345-265a-4d68-85da-89bc5f89b297&view=7&q=author:*&useFiltersInViewXml=0&FilterField1=Author&FilterValue1=${user.Title}&FilterType1=User&viewpath=/sites/DC3/Wiki/SitePages/Forms/AllPages.aspx&FilterField2=DocIcon&FilterValue2=aspx&FilterType2=Computed&FilterOp2=In' target="_blank">Wiki</a></td>
                            </tr>
                        `).join('')}
                    </tbody>
                </table>
            </div>

            <div style="padding-top:10px; display: flex; align-items: center; justify-content: flex-end">按 Esc 关闭&nbsp;&nbsp;
                <button id="copyButton" style="padding: 10px 10px; background-color: #3CB371; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px;outline: none;">复制表格</button>&nbsp;
                <button id="closeButton" style="padding: 10px 10px; background-color: #ff4d4f; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px;outline: none;">关闭</button>
            </div>
        </div>
        `;
        document.body.appendChild(container);

        const copyButton = container.querySelector('#copyButton');
        copyButton.addEventListener('click', function() {
            copyTable(document.getElementById('_rl_table'));
        });

        const closeButton = container.querySelector('#closeButton');
        closeButton.addEventListener('click', function() {
            document.body.removeChild(container);
        });
        container.addEventListener('keydown', function(event) {
            if (event.key === 'Escape') { document.body.removeChild(container); };
        });
        closeButton.focus();
    }
})();

使用方法:
打开 sharepoint.com 或 sharepoint.cn 对应的网址,然后 按 Ctrl+Q,输入关键字,即可查询。点击用户名,可以打开新窗口,查看该用户创建的所有文章。

运行效果: